There's a dungeon under this blog · Ata Kuyumcu's Blog
search
&]:hidden" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><br>&]:block" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><br>https://blog.lvmbdv.dev/posts/theres-a-dungeon-under-this-blog/Depth 1 of that day’s dungeon. Tiles from Kenney’s Tiny Dungeon, CC0.There’s a dungeon under this blog, every page of it. Find the way in and you get<br>a little pixel character (a knight, a wizard, a princess etc; it depends on a<br>hash of your peer id) standing in a sunlit village square. Through the stone<br>arch north of the well, a procedural dungeon crawl: monsters, potions, chests<br>that sometimes have teeth. If someone else is reading the blog right now, they<br>can join in the same village, the same dungeon. You can watch them wander. You<br>can shout at each other.<br>The blog is static so there is no game server, no websocket backend, no<br>database. Multiplayer is handled completely peer to peer.<br>Two browsers, no middlemen<br>WebRTC lets two<br>browsers open a data channel directly to each other. The catch has always been<br>the introduction: before peers can talk, they have to exchange connection offers<br>through something they can both reach, and that something is traditionally a<br>signaling server you run.<br>Trystero’s trick is doing the introduction<br>over infrastructure that already exists and belongs to nobody. By default it<br>uses public nostr relays; it can also ride BitTorrent<br>trackers or MQTT brokers. Everyone who opens the dungeon joins a room named<br>after the UTC date, announces themselves through the relays, and completes the<br>handshake. From then on the relays are out of the loop and game traffic rides<br>peer-to-peer.<br>The entire server-side footprint of multiplayer is one edit to the<br>Content-Security-Policy header: connect-src now allows wss: so the relay<br>websockets can connect.<br>The map stays home<br>The obvious multiplayer design has someone generate the dungeon and send it to<br>everyone else. Nobody sends anything here. Every client derives the day’s date,<br>hashes it into a seed, and feeds it to a<br>mulberry32<br>PRNG. Same seed, same rooms, same corridors, same monsters standing in the same<br>spots, on every machine that opens the dungeon that day. Depth 3 is generated<br>from the day seed xored with the depth times a golden-ratio constant, the moment<br>you reach it, so the whole descent is implied by a constant number nobody<br>transmitted.<br>What actually goes over the wire is live state: positions, hit points, the<br>occasional “I opened this chest” so loot doesn’t respawn for the next visitor. A<br>full update is under a hundred bytes.<br>The world resets at midnight UTC. Whoever is mid-crawl keeps their run, but<br>they’ve become a ghost of yesterday: new arrivals join tomorrow’s room and the<br>two eras can’t see each other. Gold is per-run either way. The dungeon keeps it<br>when you die.<br>Somebody has to be the dungeon master<br>Deterministic generation gets everyone the same starting monsters, but then the<br>monsters have to move, and with no server there’s no referee to move them. Two<br>players watching the same slime need to agree on where it went.<br>The dungeon elects one. Every peer id is a random string, so the<br>lexicographically lowest id on a given depth becomes that depth’s authority: it<br>steps the monsters every 400 ms, resolves attacks, and broadcasts the results.<br>Everyone else mirrors. When the authority closes their tab, the next-lowest id<br>notices and takes over from the last broadcast state, because every client was<br>already keeping a copy. Solo play is the degenerate case where you’re always the<br>lowest id on your floor.<br>The scheme leans on the same determinism everywhere. Mimics are the cleanest<br>example: some chests are secretly monsters, the choice of which falls out of the<br>day seed, and so every player is lied to by the same chest.<br>What a page pays<br>An easter egg on every page has to cost nearly nothing on every page. The<br>always-loaded part is a 1,057-byte script that listens for the way in and does<br>nothing else. When someone gets through the door, it dynamic-imports the actual<br>game: 94 KB of bundle (Trystero included) and a 10 KB tileset.<br>The art is Kenney’s Tiny Dungeon and<br>its sibling pack Tiny Town (the village),<br>132 CC0 tiles each, composited into one sheet; between them I’m using about<br>ninety. The sounds ship in zero bytes: every effect is a couple of<br>Web Audio<br>oscillators with a gain envelope, synthesized on the spot. Potions bubble<br>upward, damage slides downward, the mimic is two detuned sawtooths. Press m if<br>it gets to be too much.<br>The walls know which way they face<br>Most of this was built by Claude Fable in an evening of back and forth, and the<br>division of labor was what you’d hope: I said things like “make it a dungeon<br>crawler” and “the message scrolls...