
UNMAPPED: Open World
An endless open world an AI writes as you explore; remixes get ENSv2 names and Uniswap royalties.
Demo
About
UNMAPPED (無界之地) is an open-source desktop game whose map has no edge, and nothing on it exists until someone walks there. The ground streams from a seed, so walking never waits for AI. The first time a player enters an unwritten place, a language model "witnesses" it: it names the place and writes its residents, a local custom, errands and foes. The model never writes code or JSON. It writes a small game DSL (OpenUI Lang dialects for scenes, dialogue, items and chapters). A parser checks every line, clamps every number to engine limits and sends errors back for at most two repair rounds; output that still fails becomes an honest error, never a stock scene. Once written, a place is saved and never needs the model again. Anyone can make a world from a few words: the model drafts seven editable world-bible cards, concept pictures for its art direction and 3-8 story chapters, then the app builds and publishes it. Every published world is an immutable, sha256-hashed cartridge on your own disk, and every save pins one exact revision. It runs on a cloud API or on your own machine: Apple's on-device model, inside the app, writes worlds, places and chapters, and a local Qwen 3.5 4B on llama.cpp also builds worlds. Play together. What a world holds is a signed, append-only history. Invite friends with a link and they walk what you witnessed with zero model calls, even while you are offline. A small world service only orders, checks and relays signed entries; it never calls a model. When two people reach an unwritten place together, one writes it and the other watches the same text arrive. Places nobody tends fade into fog and become legends, and residents pass on rumours of what friends really did. A world kept on one device can also join friends' worlds peer to peer over WebRTC, with a TURN relay so friends behind a VPN or a strict NAT still meet, and friends chat in both. A .world file carries a whole world, so it outlives any server. It is designed so a 70-year-old can play: four title entries, one list of worlds, one "Join a world" field that takes a friend's ENS name or 6-character join code, and one passkey. On chain (Sepolia) we give AI-made worlds provenance and a remix economy: - ENSv2 names first: published worlds, remixes, saves and players get names in one tree under unmapped.eth (aether-land.unmapped.eth, moss-hollow.aether-land.unmapped.eth, kidney-run.aether-land.unmapped.eth, <you>.players.unmapped.eth). Text records hold only ids, versions and hashes; a save's name also carries its progress and join code. The app resolves names through the ENSv2 Universal Resolver, opens a world only if the hash matches the local bytes, and lets a friend join by typing the name. Naming a world deploys its own emancipated subname registry, so the family tree is the ENS hierarchy itself. - Uniswap: a world's name holder can launch it from the app. Its token is priced by a Continuous Clearing Auction (via Liquidity Launcher's LBPStrategy), denominated in its parent world's token, then graduates into a v4 pool. Our v4 hook takes 1% of every swap and pays it up the family line (50/30/20) to whoever holds each world's ENS name. Live on Sepolia: the root world's auction cleared 86% above its floor with 4 passkey bids, and a 10 USDC buy paid exactly 1% to the name holder. - No wallet and no key in the app: a player's passkey owns a smart account, and a gas station only pays gas for a fixed set of calls. - A shared world's service can record a fingerprint of each beat on chain (WorldProvenance), and the app checks it against its own copy. The chain is optional: with nothing configured, the whole game still plays.
How it's made
UNMAPPED is an Electron app in TypeScript (Bun, electron-vite, React, zustand). The same land is drawn two ways: an HD-2D diorama in Three.js (painted chunk floors, shadow-casting sprites, bloom and tilt-shift built from three/examples) and a flat 16-bit canvas. Terrain is 32x32-tile chunks computed from seed + chunk coordinates, never stored and never generated by the model. AI: every model call goes through a Cordis-based harness (modelled on DeepSeek Harness) that assembles the prompt from ordered sections and exposes tools whose only way to change the world is a typed GameEffect. The model writes OpenUI Lang, parsed by @openuidev/lang-core against zod v4 schemas; one repair round sends back every kind of mistake at once (at most two rounds), and every number is clamped. Verified end to end with OpenAI gpt-5.4-mini. Apple's on-device model runs inside the app through our Swift bridge on FoundationModels. Its 4K context is too small for the full prompt, so there the model fills a JSON Schema derived from the DSL's own zod shapes (guided generation) and the host writes the answer back as an OpenUI Lang program the parser still checks. A local Qwen 3.5 4B runs on a llama.cpp sidecar the app starts. A local usage ledger records tokens and latency for every call, never prompts or keys. Mods are YAML + markdown prompt sections and declarative tools, never JavaScript. Data: published worlds are immutable sha256-hashed cartridges and saves pin an exact revision. Portable data is encrypted with a random AES-GCM Data Key wrapped by a passkey PRF or the macOS keychain. Shared worlds: a world is one sequenced, append-only log of signed events. Every device has an Ed25519 key; an event's id is the sha256 of its canonical JSON, and each log entry chains the previous entry's hash and carries the service's receipt signature. A Bun world service orders, validates (the same zod + DSL checks the app runs) and relays entries over one WebSocket; it never calls a model and never edits an entry. Folding the log is deterministic (integers, strict ISO dates, no floating-point trig, no locale compare), so the service and every device compute the same land, fog, legends and rumours. When two people reach one unwritten place, a short claim lets one write while the other watches the stream. Invite links carry a one-time secret that never enters the log. A .world file (a zip of the log and content packs) verifies offline and can bring a world up on another service. A phone-sized browser page joins by invite with a non-extractable WebCrypto Ed25519 key. Peer to peer: worlds kept on one device join as "continents", Yjs docs over y-webrtc. Nothing is exchanged until a peer's hello matches the continent code, protocol and physics version, and every entry is validated on arrival. We patched y-webrtc so only the lower peer id initiates (upstream glare left one-way data channels). A Cloudflare Worker mints short-lived Cloudflare Realtime TURN credentials and keeps the TURN key, so friends behind a VPN still connect: behind NordVPN, STUN alone never connected and the relay joined in 2.4 s. Chain (Ethereum Sepolia): Solidity compiled with solc-js, viem only in Electron's main process (keys never reach the renderer). - ENSv2: names resolve through the Universal Resolver (DNS-encoded names). Names come first: LineageRegistry.register names a cartridge revision and deploys that name's own children's registry; recordSave names a player's save, held by their passkey account, with its hash, progress and join code as text records; player names are saves under players.unmapped.eth. Every registry it makes keeps only REGISTRAR | SET_PARENT, so every name stays an emancipated, safely transferable ERC-1155. We redesigned around this after hitting TransferUnsafeUntilRegistryIsEmancipated. - Uniswap: LineageRegistry.launch (name holder only) starts a Continuous Clearing Auction through Liquidity Launcher LBPStrategy v3.1.0 and the CCA factory v2.1.0 (Uniswap's own Sepolia deployments), with their structs mirrored field for field. LineageHook (v4-periphery BaseHook) uses beforeInitialize (only LBPStrategy may open a world x parent-token pool), afterSwap + afterSwapReturnDelta for a 1% royalty split 50/30/20 up the line, and claim() pays the current ENS name holder. LineageRouter buys or sells along a whole family line inside one unlock. - Passkeys: a PasskeyAccount verifies WebAuthn signatures with OpenZeppelin's WebAuthn library on the P-256 precompile (EIP-7951); main builds each batch and the passkey signs the digest. The gas station, a Cloudflare Worker, holds the only paying key: it builds each transaction itself, allows only a fixed list of targets, simulates first and caps gas and fees. - WorldProvenance: a world service records each beat's log hash and fingerprint (never content); the app only reads it and compares with its own copy. Hacky bits: we mine the hook address with CREATE2 in TypeScript for flags 0x2044; initializeDistribution doesn't return the auction, so we recompute its address from LBPStrategy's salt rule; the whole market (deploy, three generations of launch/auction/graduation, swaps, royalties, name transfer, refusals) is replayed against Sepolia's real contracts with eth_simulateV1 before anything spends gas; Electron dev can't reach Touch ID, so main serves a localhost page the system browser opens for the one signature. The read-only Lineage Auction House is a static page on Cloudflare Workers that reads Sepolia directly. E2E runs drive the real app over the Chrome DevTools Protocol, with a virtual WebAuthn authenticator, a virtual gamepad and touch; one run takes a single world through Create, play, sharing, a co-owner, a phone and a .world move across six clients.