How Dandle Works: Building a 3D Physics Word Game in the Browser

I'm Davis, and I build Dandle on my own. This is the developer's-eye view: what's actually happening under the game you play in a browser tab, and which part nearly broke me. If you want to know how to play, read the guide instead.

The core idea: words as physical objects

Most word games treat a word as a string to validate and score. Dandle treats it as a piece of matter. You pick a letter cube already in the world, type a word containing that letter, and it arrives as a chain of cubes welded onto the one you picked. From that moment it has mass, it sags, it levers, and the impulse of its arrival shoves everything else around.

That one decision — a word is a rigid body, not a score — is where all the interesting engineering comes from. A crossword lattice is a clean integer grid; physics is continuous, messy, and disagrees with that grid the instant anything bends.

It's all static files: no build step, no bundler, no server. index.html pulls Babylon.js and Havok from a CDN and the rest is plain ES modules served as-is. If something's broken, I can't blame a toolchain.

Letters as rigid bodies

Each letter cube is its own physics body — a Babylon PhysicsAggregate box with CUBE_MASS = 0.5, linear damping 0.3, angular damping 0.5 and near-zero restitution. There is no compound body, no single "structure" object. Adjacent cubes are joined by a Physics6DoFConstraint with minLimit = maxLimit = 0 on all six axes — a fully rigid weld — and Havok solves the whole network every step.

So why does the structure feel floppy if every joint is locked? Because a solver approximates: dozens of rigid locks fighting gravity and impulses produce something that flexes. That softness became the game's personality — a tall build sways, a long word sticking sideways sags under its own lever arm — and none of it is authored.

To keep it from turning to jelly, physics runs at a fixed 1/120 timestep in deterministic lockstep. Double the usual rate, and those extra solver passes are the difference between a structure that leans and one that explodes. Gravity is -12, because real gravity at this scale looks like syrup.

The hardest problem: grid coordinates vs. physical position

Here's the bug factory at the heart of the game. Every cube carries two notions of where it is:

Fresh and upright, the two agree and every naive check works. Once the structure deforms they diverge, and those checks misfire in infuriatingly plausible ways. A grid neighbour can be physically a metre away, so welding to it yanks the structure. A cube from the far side of the level can drift into the exact spot where your new letter is about to spawn.

The rule I settled on: the grid is the skeleton, physical distance is a sanity filter. The grid decides adjacency and occupancy, but nothing acts on that decision without confirming the cube is really there.

Merge, conflict, and building through walls

Walking the word out from the anchor, each cell resolves three ways. A cube with the same letter within 0.6 means the word fuses into it rather than spawning a duplicate — that's how words cross and share letters. A different letter within 0.5 rejects the placement. Otherwise the cell is free.

Walls are the fun exception. Building through a wall or floor is a deliberate mechanic: cells beyond static geometry ignore everything on the far side, the letter spawns inside the wall, and physics ejects it. You just can't fuse across one. A birth resolver applies capped depenetration pressure for a cube's first 2.2 seconds so it eases out rather than launching into orbit.

The deformation filter

Everything downstream gets the same treatment. Welds only form between grid neighbours physically within [0.45, 1.6] of each other, and the crossword walk — which checks that every perpendicular run of letters spells a real word — breaks past that same 1.6 gap, so a sagged limb can't form a phantom crossword with a cube it no longer touches.

Two more rules stop the structure tearing itself apart. One weld per neighbour cell: if several cubes claim the same adjacent cell, only the one nearest the ideal 1.0 spacing gets welded — otherwise two rigid locks command two cubes into one point and the whole thing launches (an old bug I clocked at 16 m/s). And merge bridges: a cube you fused into has unrelated grid coordinates, so the normal weld can't reach the word's other letters. Those links get added explicitly, with a far more generous limit, because a word splitting at its crossing point is worse than a stretched joint.

The self-untangling net

No set of rules survives contact with a physics solver, so a last line of defence runs every frame. It scans cube pairs; any two whose centres are closer than 0.82 are interpenetrating (face contact is 1.0, so this only fires on real overlap). Welded pairs are skipped. Everything else gets equal-and-opposite separating impulses, and here's the part that actually fixed it: the push escalates. The longer a pair stays jammed, the harder the shove, ramping to 10× over about three and a half seconds and resetting the instant they separate. Before escalation, cubes could sit stuck inside each other indefinitely, because the stiff welds beat any constant-strength repulsion.

My debugging window through all of this is unglamorous: verbose [PLACE], [CONSTRAINT] and [SEP] console logs, gated to real submits so the live ghost preview doesn't drown them.

Procedural music with no audio files

Dandle ships zero audio assets. Every sound is generated at runtime with the Web Audio API — oscillators, noise buffers written sample by sample, filters, envelopes.

The soundtrack is a small improvising trio. Each level gets a root note from a table of 17 keys; because 17 is prime, rotating the sequence by five each time it wraps guarantees a second pass lands in different keys. Underneath, a four-bar I–vi–IV–V progression anchors everything — the trick that makes improvised parts sound intentional rather than aimless. A triangle-wave walking bass outlines chord tones, up on even bars and down on odd. A sine melody stabs the bar's chord on the downbeat, runs a short scale figure mid-bar, and throws in syncopated fills at random. Drums are a filtered kick on 1 and 3, a bandpassed noise burst for a brush snare on 2 and 4, and a hi-hat whose velocity is randomised so it breathes instead of ticking. It feeds a filtered feedback delay at 18% wet — the difference between "synthesised" and "in a room."

My favourite piece is the adaptive tempo: beat duration is _beatDur / (1 + cubeCount × coef), which makes BPM linear in the number of cubes on the board — about 90 BPM at the five-cube start, roughly +3.5 BPM per letter, uncapped. Dividing rather than subtracting keeps the beat strictly positive and lets the per-cube effect taper, so it accelerates forever without racing away. Nobody has to be told about this; players just notice the music tightening as the tower gets ambitious.

The print look

The newspaper aesthetic is one fullscreen fragment shader doing three things. A halftone: screen coordinates rotated 45 degrees into a dot grid, each dot's radius growing where the image is darker, exactly like ink on paper. Film grain, from a cheap hash of screen position and time. A vignette, from a smoothstep on distance from centre. Dot pitch is about 7.6 pixels, exposed in settings as "Halftone size."

All of it is live-tunable from the console via window.__fx, which is how the values got picked: knobs turning with the game running, until it looked printed rather than filtered. The whole post-process is wrapped in a try/catch — a shader that won't compile on one unusual GPU should cost you an effect, not the scene.

What I'd do differently

The honest one: I once rewrote placement on a "physics is truth" model — throw away the grid, derive adjacency purely from world positions. It was cleaner, more principled, and I reverted it.

Physics-only placement lost the crossword. Without a stable integer lattice, a sagged structure's letters no longer line up in rows and columns you can validate words along, and the puzzle dissolved into approximate geometry. The hybrid is less elegant and needs a table of tuned constants, but it's the version that plays well. A principled model that produces the wrong feel loses to a patchwork that produces the right one.

The other regret is structural. Nearly the whole game lives in one enormous game.js — scene, physics, placement, levels, UI, effects, mobile keyboard. I navigate it by grep, which is fine for me and unkind to anyone else. Starting again, I'd keep the no-build deployment and split it into honest modules from day one.

Go break it

None of this matters unless the result is fun to push around, so the real test is yours. Play Dandle and try to make the structure do something I didn't plan for — build through a wall, overload a joint, watch the tempo climb. If you want the mechanics explained first, the complete guide covers them.