A multiplayer fortress game for players and a typed modding/debugging stack for developers.
Play the fortress, shape the simulation, and debug the same world from both sides of the screen.
Open Dwarf is a Dwarf Fortress–inspired browser game built for the people who want to jump into a living fortress and the people who want to shape it. It runs on a Bevy/WebAssembly engine, a Deno + Fresh web app, and WebRTC for peer-to-peer multiplayer. Developers can build custom game modes, automation bots, and debugging workflows against a stable public API. The project ships a browser automation layer for inspecting multiplayer sessions, replaying interactions, and validating game behavior across multiple clients.
Most game projects treat browser automation as throwaway test infrastructure. Open Dwarf treats it as part of the experience: the same primitives a mod author uses to extend the game are the ones a developer uses to drive it from Playwright, simulate players, and assert on game state. The goal is a reproducible, typed, developer-first interface to a complex multiplayer simulation without making the player-facing side feel bolted on.
# 1. Install Deno (https://docs.deno.com/runtime/getting_started/installation)
# 2. Build the WebAssembly engine
deno task web-release
# 3. Run the dev server
deno task dev
# → http://localhost:8000
# 4. (Optional) Run the Playwright debug examples
deno task testOpen Dwarf ships two author-facing SDKs, both targeting the same sandboxed runtime on the server. Pick the language that fits the job:
- TypeScript (
@opendwarf/sdk) — fast iteration, no build step, hot-reload from source. Best for gameplay scripting, UI logic, glue. - Rust (
opendwarf-sdk) — compiled to WebAssembly, sandboxed by the same wasm-host, with the speed, safety, and type system of Rust. Best for perf-sensitive or type-strict mods.
Both expose the same hook names (on_player_join, on_tick, …) and the
same value types. A mod in either language ends up as the same Mod
object inside the runtime.
import { createMod } from "@opendwarf/sdk";
export default createMod({
name: "welcome-mod",
version: "1.0.0",
onPlayerJoin(ctx, player) {
ctx.broadcast(`${player.name} entered the fortress.`);
},
onTick(ctx) {
for (const dwarf of ctx.players.list()) {
if (dwarf.stats.hunger > 80) {
ctx.world.spawn("food", { near: dwarf.id });
}
}
},
});use opendwarf_sdk::{export_mod, GameContext, Mod, ModManifest, Player};
#[derive(Default)]
pub struct Welcome;
impl Mod for Welcome {
fn manifest(&self) -> ModManifest {
ModManifest::new("welcome-mod", "1.0.0")
}
fn on_player_join(&self, ctx: &mut GameContext, p: &Player) {
ctx.broadcast(&format!("{} entered the fortress.", p.name));
}
fn on_tick(&self, ctx: &mut GameContext) {
for player in ctx.players().list() {
if player.stats.hunger > 80.0 {
ctx.world().spawn("food").near(player.id).fire();
}
}
}
}
export_mod!(Welcome);Build:
cd game_library/crates/opendwarf-welcome
cargo build --release --target wasm32-unknown-unknown
cp target/wasm32-unknown-unknown/release/opendwarf_welcome.wasm \
../../../examples/mods/The loader discovers .wasm and mod.ts side by side — drop the binary
next to any TypeScript mods.
import { createGameDebugger } from "@opendwarf/debugger";
const debug = await createGameDebugger({
baseUrl: "http://localhost:8000",
});
const alice = await debug.connectPlayer("Alice");
const bob = await debug.connectPlayer("Bob");
await alice.performAction("move", { x: 10, y: 5 });
await debug.waitForState((state) => state.players.length === 2);
const snapshot = await debug.captureSnapshot();
await debug.close();A mod is a TypeScript module that exports a createMod() result. Drop it in
mods/ and Open Dwarf loads it on server startup:
// mods/greeter/mod.ts
import { createMod } from "@opendwarf/sdk";
export default createMod({
name: "greeter",
onMessage(ctx, msg) {
if (msg.text === "/hello") ctx.broadcast("Hello!");
},
});See examples/mods for runnable mods.
┌────────────────────────────────────────────────────────────────────┐
│ Playwright Debugger (packages/debugger) │
│ connectPlayer · waitForState · captureSnapshot · replay │
└──────────────────────────────┬─────────────────────────────────────┘
│ drives browser clients
┌──────────────────────────────▼─────────────────────────────────────┐
│ Game Client (Fresh + Preact islands + WebGL canvas) │
└──────────────────────────────┬─────────────────────────────────────┘
│ WebRTC data channels
┌──────────────────────────────▼─────────────────────────────────────┐
│ Mod Runtime (packages/server) │
│ ▲ ▲ │
│ │ TS mods │ Wasm mods │
│ │ │ │
│ @opendwarf/sdk opendwarf-sdk (Rust → wasm32) │
│ (loaded directly) (loaded via wasm-host.ts) │
│ │
│ │ shared FFI to engine │
│ ▼ │
│ Game Engine (game_library, Rust + Bevy → WASM) │
└────────────────────────────────────────────────────────────────────┘
See docs/architecture.md for the full breakdown.
- Getting Started — install, run, write your first mod
- Modding API — hooks, context, world primitives
- Debugging with Playwright — automation layer
- Architecture — how the layers fit together
- API Reference — every exported type and function
examples/mods/welcome— TypeScript mod, react to player join/leavegame_library/crates/opendwarf-welcome— same mod, written in Rust → wasmexamples/mods/run-welcome.ts— drive the TS mod through the runtimeexamples/mods/run-welcome-rs.ts— drive the Rust wasm mod through the same runtimeexamples/debugging/multiplayer-session— 2-client Playwright flowexamples/debugging/replay-analyzer— record and replay interactions
Contributions are welcome. Please:
- Open an issue describing the change before large refactors.
- Run
deno task checkanddeno task testlocally. - Keep public SDK changes backward-compatible or document the migration.
The project uses Deno workspaces. SDK and debugger packages live under
packages/ and are published independently.
MIT