NetHack, made embeddable

A whole world.
One little tag.

Forty years of dungeon intelligence, a lovingly drawn world, and an interface that belongs to you.

Drop in the viewer. Supply a public observation. Build a spectator screen, a replay, or a home for your autonomous adventurer.

<neohack-world src="https://neohack.dev/dashboard?run=RUN_ID"
  autoplay speed="4" controls>
</neohack-world>

Shadow DOM · Pixel renderer · Read only by default

01 / Bring your own story

The world renders. Your code decides.

One module registers the component and exports the same Neonethack and Game API used by the app. Styles live inside its shadow root; the selected runtime artwork is included in the module.

This showcase never starts a game. The entrance is decorative. Replays display captured observations without spending turns or loading an engine.

Explore the library ↗

// <script type="module" src="https://neohack.dev/component/neohack.js"></script>
import Nethack from '/component/neohack.js';

const world = document.querySelector('neohack-world');
world.addEventListener('frame', ({ detail }) => {
  console.log(detail.observation.turn);
});

// Existing public API + your owned transport.
const api = world.connect(transport, { writable: true });
const game = await api.create({ name: 'Ada', seed: 42 });
world.snapshot = game.state;
await game.move('north');
02 / A message is enough

Small surface. Full vocabulary.

world.addEventListener('message', ({ data }) => {
  console.log(data.id, data.result ?? data.error);
});

world.postMessage({
  jsonrpc: '2.0', id: 'discover',
  method: 'tools/list'
});

world.postMessage({
  jsonrpc: '2.0', id: 'observe',
  method: 'tools/call',
  params: {
    name: 'neonethack_session_observe',
    arguments: { sessionId: game.id }
  }
});

Discover generated tool schemas with tools/list. Call named operations with tools/call. Each reply keeps your correlation ID and the engine’s original response.

No transport means no engine tools. Read-only connections expose only read-only tools. Execution is an explicit host choice. Messages target this element, with no global window listener.

Call world.registerWebMcp() to register the connected tools with a supported browser’s model context. Call the returned dispose() when done.

01 — DISPLAY

Just the known world

The shared renderer uses public perception. Hidden rooms stay hidden. A beautiful map never invents knowledge.

02 — REPLAY

Every recorded moment

src="https://neohack.dev/dashboard?run=RUN_ID" autoplay speed="4" controls loads a public ledger recording. Optional sound enables sound after a user gesture; loop repeats playback. Boolean attributes can use ="false" to disable them. Speed is 0.5–5×, default 4× (16 frames/second). You can also use loadReplay(frames), seek(index), play(250), and pause(). Also available as replay.* messages.

03 — BUILD

Write the next ascender

Use the same game API in the Ascender workshop. Edit a strategy, choose a character, and watch a real test run.