There’s a dungeon under this blog, every page of it. Find the way in and you get a little pixel character (a knight, a wizard, a princess etc; it depends on a hash of your peer id) standing in a sunlit village square. Through the stone arch north of the well, a procedural dungeon crawl: monsters, potions, chests that sometimes have teeth. If someone else is reading the blog right now, they can join in the same village, the same dungeon. You can watch them wander. You can shout at each other.

The blog is static so there is no game server, no websocket backend, no database. Multiplayer is handled completely peer to peer.

Two browsers, no middlemen

WebRTC lets two browsers open a data channel directly to each other. The catch has always been the introduction: before peers can talk, they have to exchange connection offers through something they can both reach, and that something is traditionally a signaling server you run.

Trystero’s trick is doing the introduction over infrastructure that already exists and belongs to nobody. By default it uses public nostr relays; it can also ride BitTorrent trackers or MQTT brokers. Everyone who opens the dungeon joins a room named after the UTC date, announces themselves through the relays, and completes the handshake. From then on the relays are out of the loop and game traffic rides peer-to-peer.

The entire server-side footprint of multiplayer is one edit to the
Content-Security-Policy header: connect-src now allows wss: so the relay
websockets can connect.

The map stays home

The obvious multiplayer design has someone generate the dungeon and send it to everyone else. Nobody sends anything here. Every client derives the day’s date, hashes it into a seed, and feeds it to a mulberry32 PRNG. Same seed, same rooms, same corridors, same monsters standing in the same spots, on every machine that opens the dungeon that day. Depth 3 is generated from the day seed xored with the depth times a golden-ratio constant, the moment you reach it, so the whole descent is implied by a constant number nobody transmitted.

What actually goes over the wire is live state: positions, hit points, the occasional “I opened this chest” so loot doesn’t respawn for the next visitor. A full update is under a hundred bytes.

The world resets at midnight UTC. Whoever is mid-crawl keeps their run, but they’ve become a ghost of yesterday: new arrivals join tomorrow’s room and the two eras can’t see each other. Gold is per-run either way. The dungeon keeps it when you die.

Somebody has to be the dungeon master

Deterministic generation gets everyone the same starting monsters, but then the monsters have to move, and with no server there’s no referee to move them. Two players watching the same slime need to agree on where it went.

The dungeon elects one. Every peer id is a random string, so the lexicographically lowest id on a given depth becomes that depth’s authority: it steps the monsters every 400 ms, resolves attacks, and broadcasts the results. Everyone else mirrors. When the authority closes their tab, the next-lowest id notices and takes over from the last broadcast state, because every client was already keeping a copy. Solo play is the degenerate case where you’re always the lowest id on your floor.

The scheme leans on the same determinism everywhere. Mimics are the cleanest example: some chests are secretly monsters, the choice of which falls out of the day seed, and so every player is lied to by the same chest.

What a page pays

An easter egg on every page has to cost nearly nothing on every page. The always-loaded part is a 1,057-byte script that listens for the way in and does nothing else. When someone gets through the door, it dynamic-imports the actual game: 94 KB of bundle (Trystero included) and a 10 KB tileset.

The art is Kenney’s Tiny Dungeon and
its sibling pack Tiny Town (the village),
132 CC0 tiles each, composited into one sheet; between them I’m using about
ninety. The sounds ship in zero bytes: every effect is a couple of
Web Audio
oscillators with a gain envelope, synthesized on the spot. Potions bubble
upward, damage slides downward, the mimic is two detuned sawtooths. Press m if
it gets to be too much.

The walls know which way they face

Most of this was built by Claude Fable in an evening of back and forth, and the division of labor was what you’d hope: I said things like “make it a dungeon crawler” and “the message scrolls could be punchier,” and it wrote the netcode and then proved the netcode worked by launching two headless browser that found each other through real public relays and screenshotted themselves seeing each other. Watching two robots meet in your dungeon is a strange kind of code review.

My actual contribution to correctness was stuff like noticing the walls looked wrong. Kenney’s tiles are directional: some bricks carry an edge shadow meant for the boundary where a wall meets the void, and the first version sprinkled them anywhere, so south-facing walls wore east-facing textures. The fix came from the tileset itself. The zip ships a Tiled sample map, and decoding it (gids, flip flags and all) revealed the intended grammar: walls that touch floor are plain seamless brick, ornament goes on south faces only, and a dripping gargoyle expects a goo basin on the tile below it. The renderer now follows the grammar, and the gargoyles drip where they’re supposed to.

Shouting into the dark

A shared dungeon where you never find anyone is a lonely piece of engineering, so there’s a shout button. Shouting broadcasts your position, and everyone on your floor hears a two-note call panned left or right by where you are relative to them, faded by distance, with a compass line in the message scroll and a brief mark over your head that ignores line of sight. Sound goes through walls; that’s the point of shouting.

If you’re on a different floor the shout arrives pitched instead: deeper from below, thinner from above. Your ears tell you which way the stairs are. The whole thing is a StereoPannerNode and some arithmetic, and it’s the feature I’m fondest of.

There’s an analytics event that fires the first time a session meets another player, so I’ll know if two strangers ever actually find each other down there. I have modest expectations.

As for the way in: the classics. One is older than the web, and you press it rather than type it. The magic words work too: one for the deliberate, one for the adventurers, one for the dwarves, one for the doomed. No keyboard? Tap the footer like you’re impatient. If you meet someone, shout.