A browser-based roguelike synchronized with crypto block timing. A run is a race against the next block: reach the exit before the block lands, or the house wins. The same codebase runs the solo dungeon, the social Tavern, and operator-selected PvP and race rulesets, with separate free and paid leaderboards. Self-hostable under the MIT License. See README.md to run it, docs/ARCHITECTURE.md for the design, and docs/DEPLOY.md to operate an instance.
7.5 KiB
Architecture
Wowngeon is a provably fair, block-timed roguelike with crypto entry and payouts. A solo run is a race against a random block deadline; multiplayer matches run the same engine under a ruleset. The game also acts as a multi-chain on-ramp for the Smirk wallet.
This page is the index for the architecture docs. Each subsystem has its own page.
System map
┌──────────── players (browser) ─────────────┐
│ index.html · tavern.html · match.html │
│ html/js/render (render kit) │
└───────────────┬───────────────┬────────────┘
Socket.IO │ │ REST (auth, payments, status)
┌────────────────▼───────────────▼────────────┐
│ src/network/* socket handlers, chat, │
│ tavern, match, payments │
├─────────────────────────────────────────────┤
game core │ src/game/* dungeon · rulesets · modes │ → RULESETS.md
money │ src/payments/* providers · wallet-rpc │ → PAYMENTS.md
cosmetics │ src/multiplayer/entitlements + catalog │ → MONETIZATION.md
chains │ src/chain/chainProfile.js │ → chain profiles, below
chat │ src/network/chat/* local + nostr │ → CHAT_AND_NOSTR.md
└─────────────────────────────────────────────┘
│ │
PostgreSQL Wownero/Monero wallet-RPC, BTCPay/checkout gateways
src/index.js is the composition root: it builds the Express app and Socket.IO server, wires the
database manager, wallet RPC service, payment config, game mode manager and socket handlers, and
mounts the REST routes in src/routes/.
Subsystem guide
| Area | Where | Doc |
|---|---|---|
| Credits, cosmetic catalog, entitlements | src/multiplayer/entitlements.js, src/services/catalogService.js |
MONETIZATION.md |
| Chain constants for the five Smirk chains | src/chain/chainProfile.js |
below |
| Payment providers and payouts | src/payments/ |
PAYMENTS.md |
| Rulesets and match engine | src/game/rulesets/, src/multiplayer/MatchEngine.js |
RULESETS.md, MATCH_MODE.md |
| Tavern lobby and spectating | src/network/tavernManager.js, src/multiplayer/Room.js |
TAVERN_AND_MULTIPLAYER.md |
| Chat, local and nostr | src/network/chat/ |
CHAT_AND_NOSTR.md |
| Client render kit and art packs | html/js/render/ |
RENDER_PACKS.md |
| Balance and pacing | src/sim/, src/game/difficultyConfig.js |
BALANCE_SIM.md, MULTI_LEVEL.md |
| Deployment and operations | scripts/deploy/ |
DEPLOY.md, DEPLOY_INSTANCES.md, LOGS_AND_BACKUP.md |
| Financial reconciliation and exports | src/services/ |
FINANCIAL_RECONCILIATION.md, FINANCIAL_EVENT_EXPORT.md |
Optional subsystems are environment-gated and dormant by default: payment gateways register only
when their *_URL / *_STORE_ID / *_API_KEY trio is set, global nostr chat requires
NOSTR_CHAT_ENABLED=true, and match rooms require MATCH_ENABLED=true. With none of those set the
server runs the native wallet-RPC payment path, local socket chat, and solo play only. The full
environment template is src/.env.example.
Chain profiles
src/chain/chainProfile.js is the single source of truth for per-chain constants across BTC, LTC,
XMR, WOW and GRIN: decimalsFor, meanBlockTimeMsFor, familyFor (monero / utxo /
mimblewimble), uriSchemeFor and atomicDivisor (BigInt).
| Chain | Decimals | Mean block time | Family | URI scheme |
|---|---|---|---|---|
| WOW | 11 | 5 min | monero | wownero |
| XMR | 12 | 2 min | monero | monero |
| BTC | 8 | 10 min | utxo | bitcoin |
| LTC | 8 | 2.5 min | utxo | litecoin |
| GRIN | 9 | 1 min | mimblewimble | grin |
An unrecognized chain id resolves to a default profile shaped like XMR: 12 decimals, 120000 ms mean
block time, monero family, monero URI scheme. Callers therefore never crash on a mis-set
CRYPTO_TYPE. Adding a chain is one entry in the registry plus a family adapter.
Money math is exact: src/money/atomic.js operates on BigInt atomic units parameterized by
decimals, never floats.
Game balance and pacing
A solo run is bounded by a random deadline drawn from an exponential distribution with mean equal to
the chain's block time, so the house edge is a convolution of the run-completion distribution with
block cadence rather than a number that can be set by hand. src/sim/ drives the real engine with
headless bots and integrates the deadline out analytically, reporting a measured house win rate per
preset and per network.
NETWORK_TUNING in src/game/difficultyConfig.js scales dungeon depth to block time. Dungeon size
and monster behaviour come from the difficulty preset; the per-network entry overrides only the
level count:
| Chain | Levels |
|---|---|
| GRIN | 1 |
| XMR | 1 |
| LTC | 2 |
| WOW | 2 |
| BTC | 3 |
NETWORK_TUNING_DISABLED=true bypasses the override and leaves the preset's own level count in
place. Descending emits a game_event, which the client handles in
html/js/network/socketHandlers.js to surface level progress. See BALANCE_SIM.md
and MULTI_LEVEL.md.
Client render kit
html/js/render/ (exposed as RK) draws a shared Scene model in four techniques: Tiled, ASCII,
Iso and 3D. Art packs are interchangeable data, a pure-canvas FX layer supplies torch and hazard
effects, and the pack and technique available to a player come from the operator-owned cosmetic
catalog. Tiled and ASCII are free; Iso and 3D are premium. See RENDER_PACKS.md.
Provable fairness
src/game/provablyFair.js derives each dungeon from a committed seed, src/game/fairnessVerifier.js
exposes verification endpoints, and src/views/verifyPage.js renders the public verification page so
a player can reproduce a dungeon from its published proof.
Conventions
- Game id is a UUID stored in
games.dungeon_seed;games.idis a serial integer. Match game objects withWHERE dungeon_seed = $N, neverWHERE id = $N. - Socket ids are volatile and change on refresh. Stable identity is the database
user_idor the session, neversocket_id. - WOW has 11 decimals (atomic divisor 1e11), unlike Monero's 12.
- Cosmetic entitlements are per-pack, resolved by tier and by lifetime credits purchased in
src/multiplayer/entitlements.js. Buying credits does not by itself unlock premium packs. - Migrations live in
src/migrations/NNN_*.sqland run in numeric order. - Tests:
cd src && npm test(Jest,--runInBand).
Source files carry JSDoc for API-level detail; these pages cover the seams between subsystems and the operator-facing intent behind them.