A 2D physics puzzle where you carve pathways through reactive geological layers to guide a volume of viscous fluid down to a collector crystal.
What it is
You are a subterranean pathfinder. Each level begins with a volume of fluid sealed in a chamber near the surface and a collector crystal buried somewhere below it. Between the two sits a cross-section of ground: watertight clay, loose sand that collapses when you cut into it, and slabs of bedrock you cannot touch.
Your only verb is digging — one finger, one continuous swipe, material removed along the path. There is no undo and no way to put earth back. A channel cut in the wrong place is a permanent feature of the level, which is what makes the digging a decision rather than a chore.
It conserves its volume, pools against watertight layers, and builds pressure in deep vertical columns — which is what drives it hard through narrow gaps at the bottom of a shaft.
Terrain is not collision data. Sand slumps to its angle of repose and drinks the fluid; fractured rock shatters into chunks that can block the passage you just opened.
The simulation runs in real time, but nothing hurries you. The tension comes from what the fluid is already doing while you decide where to cut next.
In action
The intended play screen: portrait cross-section, minimal instrument HUD, fluid working its way down a channel that has just broken through the sand band.
Before / after
The same level, same fluid volume, same layers — shown with and without the player's intervention. Left is the level as it loads. Right is the level after one considered channel, cut down the centre between the bedrock ribs and landing inside the collector trough.
FAIL — 0% collected. The clay is watertight, so the payload never leaves the chamber. Nothing about the level resolves itself without a cut.
CLEAR — 92% collected. The remaining 8% is the wet-sand halo: the fluid the permeable band drank on the way down. Cut wider and you lose more of it to the sand; cut off-centre and the cavern floor sends the rest to the drains.
Ground
Every layer answers two separate questions: what happens when the dig tool passes through it, and what happens when the fluid reaches it. The puzzles come from those answers disagreeing.
How it is meant to work
Sand that slumps, fluid that conserves volume, and rock that breaks into rigid bodies do not come from one solver. The specification pairs two.
The playfield is a dense 2D grid; each cell carries a material type, velocity, mass and wetness. This is the falling-sand approach, and it is what makes thousands of interacting sand, clay and fluid cells affordable.
A conventional 2D physics engine handles the fractured-rock chunks. The two layers exchange forces: the grid pushes buoyancy and pressure into the bodies, and the bodies mask out grid cells.
The level is split into chunks, and chunks with nothing moving in them sleep. Resting sand and untouched clay cost nothing per frame.
Grid updates are dispatched to worker threads or compute shaders, with an ECS keeping particle data separate from the systems that act on it — the shape that keeps this viable on a phone.
Levels are authored as PNGs, with hex values mapping to starting cell
states — #FF0000 clay, #00FF00 sand,
#0000FF fluid spawner, #FFFFFF collector — so
a level is data a designer can paint rather than code.
Full specification →
Getting started
What you can do with Subsurface today depends entirely on where you're trying to do it, and the differences are worth stating plainly.
The design document is the project's source of truth: mechanics,
material behaviours, simulation architecture, level format and
progression. Nothing to install — read it
rendered on GitHub, or open docs/design-spec.md in a checkout.
# clone and open — no build step, no dependencies
git clone https://github.com/CoderCoop/SubSurface.git
cd SubSurface
open docs/index.html # macOS
xdg-open docs/index.html # Linux
start docs\index.html # Windows
Why this one differs: the site is plain HTML and CSS with no
generator, no bundler and no network calls, so a browser can open it
straight off the filesystem. You would only need a local server
(python3 -m http.server -d docs) if the site later
starts using ES modules or fetching level data — both of which
browsers block on file:// origins under CORS, even
though the files sit right there.
Nothing to install — open it in a browser. It is a progressive web app, so on a phone you can add it to your home screen from the browser menu and it will run full-screen and offline afterwards.
Why this one differs: it is the only entry here that needs no
checkout, because the game ships as part of this site rather than
alongside it. Everything it needs, including the physics engine, is
served as static files from docs/play/ — there is no
server, no API and no network call once the page has loaded, which
is exactly why it can be cached and played with no connection.
# fetches the physics engine (Box2D, via planck)
npm install
# the game, straight off the filesystem
open docs/play/index.html # or xdg-open / start
# and the tests (Node 18+)
npm test
Drag to dig. Clay cuts clean and holds fluid. Sand collapses into whatever you cut and drinks what runs through it, giving it back only under pressure. Fractured rock shatters into loose chunks that can wedge in the passage you just opened. Get 85% of the payload into the crystal.
Why this one differs: the page itself needs no install — the
physics engine is committed alongside the game, so it opens straight
off the filesystem like everything else here.
npm install is for the test suite, which resolves the
same engine from node_modules rather than the vendored
copy. A test fails if those two ever drift apart. There is still no
bundler anywhere: planck ships a plain UMD file that works under
both a script tag and require.
Open the game on your phone and add it to your home screen — "Add to Home Screen" in Safari's share menu, "Install app" in Chrome's menu. It then launches full-screen with no browser chrome and runs with no connection.
Why this one differs: there is still no App Store or Play Store listing, and no TestFlight or internal-testing track — this page will not link to one until a build exists. What changed is that it no longer needs to. A progressive web app installs from the browser, so the mobile packaging, review queues and signing certificates the spec's native target implies are simply not on the path any more. The remaining gap is the game itself — one level, raw grid, no art, no audio, no haptics — rather than the delivery.
Status
| Area | State | Notes |
|---|---|---|
| Design specification | Written | Mechanics, materials, architecture, level format, progression. |
| Project website | Written | This page. Illustrated with mockups until a build exists. |
| Cellular grid (fluid, sand, clay) | Proof of concept |
Working, in docs/play/ — fluid flow and pressure, sand
at its angle of repose, absorption and pressure release,
digging, and the collector. Runs in a browser; covered by 32
tests, including the conservation invariant after every step.
|
| Rigid-body layer (fractured rock) | Proof of concept | Box2D (via planck) drives the chunks; the interaction layer is ours. Fluid exerts buoyancy and drag on the bodies, and the bodies mask out grid cells so every cellular rule treats a chunk as solid without knowing rigid bodies exist. |
| PNG level loader | Not started | Hex-to-material mapping described in the spec. |
| Audio & haptics | Not started | ASMR-leaning digging and flow; pressure rumble, fracture taps. |
| Volume conservation vs. absorption | Decided |
Wet sand holds fluid rather than destroying it, and
gives it back when enough pressure builds against the band. So
volume is strictly conserved —
released = in play + collected + drained + held —
and a saturated band is a delay and a reservoir rather than a
leak. Only the collector and the drains take fluid off the
board, and both are counted.
|
| Engine for the shipping build | Open | Deliberately undecided. The proof of concept answered the questions it was built to answer; whether the game ships on it or is ported to a game engine is a separate call. Either way Subsurface stays open, which narrows the field. |
| Level format | Interim | Levels are currently bands expressed in code. The PNG loader below replaces that with the same data painted as pixels. |