From 343c1f689b1b8abe4c06a275ac90a8b5d07de25f Mon Sep 17 00:00:00 2001 From: jwinterm Date: Fri, 10 Jul 2026 22:24:37 -0400 Subject: [PATCH] docs: architecture pass for the 5-pillar build + autodocs setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hand-authored architecture docs (the 'why'/seams that autodocs can't capture): - docs/ARCHITECTURE.md index + system map + the 5 pillars + chain profiles + conventions - docs/CHAT_AND_NOSTR.md ChatProvider seam, bridge vs per-player signing, two-relay tier model, verifyChatEvent impersonation guard, relay policy - docs/PAYMENTS.md provider contract + registry, BTCPay/checkout/native, hot path - docs/RULESETS.md Ruleset data object, win-condition strategies, PvP, MatchRoom seam - docs/MONETIZATION.md credits, cosmetic_catalog, the four-way unlock rule, premium->tier Autodocs (reference layer): added a docs:api npm script (jsdoc-to-markdown) that regenerates docs/API.md from the existing JSDoc. Enable with 'cd src && npm install' then 'npm run docs:api'. Hybrid by design — auto reference + hand-authored architecture. No runtime code changed. Suite 56 suites / 407 tests green. --- docs/ARCHITECTURE.md | 70 +++++++++++++++++++++++ docs/CHAT_AND_NOSTR.md | 127 +++++++++++++++++++++++++++++++++++++++++ docs/MONETIZATION.md | 71 +++++++++++++++++++++++ docs/PAYMENTS.md | 85 +++++++++++++++++++++++++++ docs/RULESETS.md | 80 ++++++++++++++++++++++++++ src/package.json | 4 +- 6 files changed, 436 insertions(+), 1 deletion(-) create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/CHAT_AND_NOSTR.md create mode 100644 docs/MONETIZATION.md create mode 100644 docs/PAYMENTS.md create mode 100644 docs/RULESETS.md diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..85ab067 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,70 @@ +# Architecture + +Wowngeon is a provably-fair, block-timed roguelike that doubles as a **multi-chain on-ramp for the +Smirk wallet**. This is the index for the architecture docs; each subsystem below has its own page. + +## The map + +``` + ┌──────────── players (browser) ────────────┐ + │ index.html · tavern.html · render kit │ + └───────────────┬───────────────┬────────────┘ + Socket.IO │ │ REST (auth, 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 relays +``` + +## The five pillars (2026-07 "10x" build) + +| Pillar | What | Doc | +|--------|------|-----| +| **P1 — Credits & catalog** | Operator-owned cosmetic catalog + tiered unlock rule (free / grant / spend-threshold / tier) | [MONETIZATION.md](MONETIZATION.md) | +| **P2 — Chain profiles** | One registry for the 5 Smirk chains (decimals, block time, adapter family, URI scheme) | see *Chain profiles* below | +| **P3 — Modular payments** | Pluggable payment providers: BTCPay + xmr/wowcheckout (Greenfield) + native Monero, behind one registry | [PAYMENTS.md](PAYMENTS.md) | +| **P4 — Ruleset / PvP** | Gameplay as a data object; race / last-alive / score / co-op are rulesets on the match engine | [RULESETS.md](RULESETS.md) | +| **P5 — Nostr chat** | Global cross-server chat over `relay.smirk.cash`; bridge → per-player Smirk-npub identity | [CHAT_AND_NOSTR.md](CHAT_AND_NOSTR.md) | + +All five are **behavior-preserving and env-gated** — each stays dormant (native/local defaults) until +its config is set, so `main` is always deployable. + +## Chain profiles (P2) + +`src/chain/chainProfile.js` is the single source of truth for per-chain constants across BTC, LTC, +XMR, WOW, GRIN: `decimalsFor`, `meanBlockTimeMsFor`, `familyFor` (monero / utxo / mimblewimble), +`uriSchemeFor`, `atomicDivisor` (BigInt). Unknown chains fall back to a WOW-shaped default. Money math +stays exact via `src/money/atomic.js` (BigInt, decimals-parameterized) — never floats. + +> **Gotcha (open):** `difficultyConfig` threads `cryptoType` everywhere but doesn't yet key difficulty +> off mean block time — it keys off payment mode. Wiring block-time → dungeon size is a pending +> game-balance change (see RULESETS.md), and it bites when Grin (~1 min) / BTC (~10 min) ship. + +## Regenerating the API reference (autodocs) + +The code is heavily JSDoc-commented. To regenerate a Markdown API reference from those comments: + +```bash +cd src && npm install # first time, pulls jsdoc-to-markdown (devDependency) +npm run docs:api # writes docs/API/*.md +``` + +Autodocs cover the **reference** layer (what each function/param is). The **why** — the seams and +flows in the pages above — is hand-authored and lives here in `docs/`. Keep both. + +## Conventions worth knowing + +- **Game id** is a UUID stored in `games.dungeon_seed`; `games.id` is a serial int. Match game objects + with `WHERE dungeon_seed = $N`, never `WHERE id = $N`. +- **Socket ids are volatile** (change on refresh). For stable identity use the DB `user_id` / session, + never `socket_id`. +- **WOW has 11 decimals** (atomic divisor 1e11), not Monero's 12. +- **Tests**: `cd src && npm test` (jest, `--runInBand`). Migrations live in `src/migrations/NNN_*.sql`. diff --git a/docs/CHAT_AND_NOSTR.md b/docs/CHAT_AND_NOSTR.md new file mode 100644 index 0000000..485b781 --- /dev/null +++ b/docs/CHAT_AND_NOSTR.md @@ -0,0 +1,127 @@ +# Chat & Nostr (P5) + +Chat has two layers: **local delivery** (Socket.IO + Postgres history) and an optional **nostr fan-out** +that carries the same conversation across servers and into the Smirk ecosystem. Both sit behind one +seam so callers never change. + +## The seam + +`src/network/chat/ChatProvider.js` — the abstraction every chat caller uses: + +``` +publish(msg) deliver + (optionally) persist a message +getHistory({scope, limit}) recent messages, oldest → newest +relaySignedEvent(req) deliver a CLIENT-signed nostr event (per-player identity) +initialize() / shutdown() +``` + +Two implementations: + +- **`SocketChatProvider`** — the default. Global scope → persist via `ChatHistoryManager` + broadcast + (`chat_broadcast`); room scope (tavern) → deliver to that Socket.IO room only. +- **`NostrChatProvider`** — a **decorator** around a local provider. Everything still delivers in-game + + persists; on top of that, global messages fan out to nostr relays, and messages arriving from a relay + are delivered locally-only (never re-published → no echo loops). + +`buildChatProvider({local, env})` (`src/network/chat/index.js`) picks: with `NOSTR_CHAT_ENABLED` unset it +returns the plain local provider (behavior-preserving); enabled + `scope=global` wraps it in a +`NostrChatProvider`. + +## Unified global chat + +The **tavern shares the lobby's global chat** — `TavernManager` takes the lobby's `chatProvider` and +publishes to the `global` scope, and sends the global backlog (`chat_history`) on join. So lobby and +tavern are one conversation with persistent history, and when nostr is on, tavern chat rides the relay +too. Topology is always **client → server → relay**: the server stays the moderation authority (bans, +rate-limit, history in Postgres); the relay is transport + fan-out. + +## Signer models + +`NostrChatProvider` takes an injectable **signer** (`{ pubkey, sign(template) }`). That one object is the +whole difference between the two identity models: + +### Phase 1 — bridge (shipped, live) +The server signs every global message with one allowlisted bridge npub; the player's name rides in an +`['n', name]` tag. Works for anyone (even anon), but on nostr it reads as one account talking for +everyone. `createBridgeSigner(NOSTR_BRIDGE_SK)` builds it; the npub must be write-allowlisted on the relay +(see *Relay policy*). + +### Phase 2 — per-player signing (built, needs browser verification) +The **client** signs with the player's own Smirk npub and sends the finished event; the server verifies + +relays it. On nostr it's genuinely *them*. + +``` +client server (chatHandler) relay + │ build kind-1 event │ │ + │ window.smirk.signNostrEvent │ │ + │ ── chat_signed {event} ────► │ rate-limit │ + │ │ verifyChatEvent(event, npub) │ + │ │ sig ok? id ok? fresh? tag ok? │ + │ │ pubkey === session npub? ◄── the │ + │ │ impersonation guard │ + │ │ escape + relaySignedEvent ──────────►│ (published under the player's npub) + │ ◄──────── chat_broadcast (in-game, all clients) ──────────────────│ +``` + +- **`src/utils/verifyChatEvent.js`** — the security core. Same defenses as `verifyNip98Event` (rebuild a + clean event, independently recompute the id, verify schnorr, freshness) **plus** `event.pubkey` must + equal the session's authenticated npub (`users.smirk_public_key`, set at NIP-98 login). A valid signature + by *some other* key is rejected (`pubkey-mismatch`). +- **`chatHandler.handleSignedChatMessage`** — rate-limit → resolve session npub → verify → escape → relay. +- **`NostrChatProvider.relaySignedEvent`** — local delivery + `transport.publish(signedEvent)` (no + re-signing) + echo-dedupe on the event id. +- **`html/js/smirkChatSign.js`** — feature-detected client helper. Signs via `window.smirk.signNostrEvent`, + falls back to the unsigned send when Smirk isn't connected, so wiring it into a send site is always safe. + +## The tier model (two relays) + +Everyone signed-in **self-signs as themselves**; the tier is only which relay accepts them, and what perks +come with it (perks + cosmetics, never identity): + +| Player | Signs | Lands on | +|--------|-------|----------| +| Anon | nobody | local only | +| Free "pleb" | own npub | `relay.smirk.cash` — open signup + PoW | +| Premium | own npub + ✦ badge | `relay.wowne.ro` — premium-post | + +The game publishes each signed event to **all** `NOSTR_RELAYS` and lets each relay's policy accept or +reject — no per-player routing or allowlist in game code. Reading is the mirror (subscribe to all, dedupe +by event id). Premium membership also sets the player's **entitlement tier**, which unlocks cosmetics +(see [MONETIZATION.md](MONETIZATION.md)) — one subscription, two payoffs. + +## Config (env) + +| Var | Meaning | +|-----|---------| +| `NOSTR_CHAT_ENABLED` | master switch (unset ⇒ plain local chat) | +| `NOSTR_CHAT_SCOPE` | `global` (fan out) or `local` (plain provider) | +| `NOSTR_RELAYS` | CSV of relay URLs; transport publishes/subscribes to all | +| `NOSTR_CHAT_TAG` | shared channel topic (default `wowngeon-global`) | +| `NOSTR_BRIDGE_SK` | bridge secret key (nsec/hex); omit ⇒ receive-only | + +## Relay policy (smirk-backend-core) + +`relay.smirk.cash` is a nostr-rs-relay gated by the Smirk backend's admission oracle. Write policy +`premium-post` = only premium (or write-allowlisted) npubs may publish; `RELAY_WRITE_ALLOWLIST_NPUBS` +(comma-separated npub1…/hex) exempts specific keys — that's how the **bridge npub is registered**. PoW +(`RELAY_INBOUND_POW_BITS`) only gates non-registered authors, so allowlisted/premium npubs post freely. +Reading is open. + +## Loader shim + +`nostr-tools` pulls ESM-only deps. `src/utils/nostrLoader.js` `loadNostrTools()` require()s it in prod +Node 22 and falls back to the pre-bundled IIFE under CJS-only Jest. Chat + NIP-98 both use it. + +## Files + +``` +src/network/chat/ChatProvider.js seam (+ relaySignedEvent default) +src/network/chat/SocketChatProvider.js local delivery + history +src/network/chat/NostrChatProvider.js decorator: local + nostr fan-out + signed relay +src/network/chat/index.js buildChatProvider (env → provider) +src/network/chat/nostr/NostrTransport.js SimplePool wrapper (multi-relay, degrades to noop) +src/network/chat/nostr/bridgeSigner.js server bridge signer +src/utils/verifyChatEvent.js verify a client-signed event (impersonation guard) +src/utils/nostrLoader.js nostr-tools loader shim +html/js/smirkChatSign.js client per-player signing helper +``` diff --git a/docs/MONETIZATION.md b/docs/MONETIZATION.md new file mode 100644 index 0000000..9038e99 --- /dev/null +++ b/docs/MONETIZATION.md @@ -0,0 +1,71 @@ +# Monetization — credits, catalog & entitlements (P1) + +One unit of account (**credits**), one operator-owned **catalog**, and a tiered **entitlement** rule that +decides who has unlocked which cosmetics — and now doubles as the premium-subscription payoff. + +## Credits + +Credits are the unit of account. A direct/single-game entry is modeled as *buy 1 credit + spend 1 credit* +(`gameModeManager.recordDirectEntryPurchase`) so it advances `total_credits_purchased` and unlocks the same +threshold cosmetics as a real credit purchase. Payouts are orthogonal to credits. + +## The catalog + +`cosmetic_catalog` (migration `024`) is the operator-owned product table — it replaces the old hardcoded, +split-brain `PACKS` (server + client). Columns: `pack_id, label, kind, projection, tier, unlock_min_credits, +grant_only, sort_order, active, metadata`. + +`src/services/catalogService.js` loads + caches it, falling back to `DEFAULT_CATALOG` if the table is +missing (42P01) or empty — so the game runs before the migration and never hard-fails on catalog reads. +The server ships `catalogSummary()` to the client, which merges it into `RK.PACKS` — the client no longer +owns a parallel catalog. + +## The unlock rule + +`src/multiplayer/entitlements.js` — `snapshotForUser(user, grants, catalog)`. A pack unlocks if **any** of: + +``` +1. it's free (tier 0, no gate) +2. an explicit grant (products → grants) +3. lifetime spend ≥ the pack's unlock_min_credits threshold +4. the user's premium TIER ≥ the pack's tier +``` + +`TIER_OF = { free:0, credits:0, supporter:1, premium:2, operator:3 }`. Clause 4 is the premium hook. +**Buying credits does NOT put you on the premium ladder** (`credits` → tier 0) — that was the old bug where +any purchase unlocked every premium pack. + +## Premium subscription → cosmetics + +`levelForUser(user)` returns the **higher** of the stored `premium_level` and an active +`subscription_tier`. So a live **wowne.ro premium subscription** sets the entitlement tier, which unlocks +tile/customization packs at/below it — **one subscription drives both chat perks and cosmetics** (see +[CHAT_AND_NOSTR.md](CHAT_AND_NOSTR.md)). + +Everything downstream — pack gating, the served catalog, the client unlock UI — already consumes `tier`, so +"premium buys cosmetics" is a **data-source** change: populate `user.subscription_tier` from a subscription +check (against the premium backend / npub) when the user is loaded. Absent ⇒ unchanged legacy behavior. + +``` +active premium sub ──► user.subscription_tier ──► levelForUser ──► entitlement tier + ├─ relay: chat perks (badge, no-PoW, VIP) + └─ catalog: tile / skin / customization unlocks +``` + +## Files + +``` +src/migrations/024_cosmetic_catalog.sql operator-owned catalog table +src/services/catalogService.js load/cache + DEFAULT_CATALOG fallback +src/multiplayer/entitlements.js snapshotForUser · levelForUser · TIER_OF · unlock rule +src/network/identityService.js entitlementsForUser (loads catalog + grants) +src/game/gameModeManager.js recordDirectEntryPurchase (direct entry = 1 credit) +html/js/render/assetPacks.js client consumes the served catalog +``` + +## Open decisions + +- Premium tiers: one flat "premium," or a ladder (supporter/premium/operator) where each rung unlocks + progressively more cosmetics *and* chat perks? (`TIER_OF` already has the rungs.) +- Where `subscription_tier` is sourced (sync a `users` column vs. live oracle query against the premium + backend by npub). diff --git a/docs/PAYMENTS.md b/docs/PAYMENTS.md new file mode 100644 index 0000000..c59ded7 --- /dev/null +++ b/docs/PAYMENTS.md @@ -0,0 +1,85 @@ +# Payments (P3) + +The game no longer implements per-chain wallet/daemon code inline. It talks to pluggable **payment +providers** behind one contract, so an operator routes each chain to whichever backend they run. + +## The contract + +`src/payments/providers/paymentProvider.js`: + +``` +createInvoice({chain, amountAtomic, description, userId, orderId}) + → {invoiceId, address, uri, amountAtomic, expiresAt, raw} +getInvoiceStatus(ref) → {status, complete, paidAtomic, confirmations, raw} +startWatch(ref, onUpdate, intervalMs) / stopWatch(ref) +sendPayout({chain, address, amountAtomic}) → {txids, raw} +validateAddress(chain, address) → {valid} +supportsChain(chain) → bool +``` + +`PaymentProviderRegistry` routes `chain → providerId` (operator-configurable) with a support-based +fallback: if the routed provider can't serve a chain, any registered provider that *can* is used. + +## The providers + +| Provider | id | Serves | Notes | +|----------|-----|--------|-------| +| **BTCPay Greenfield** | `btcpay` | BTC, LTC | `src/payments/providers/btcpayProvider.js` — real BTCPay Server. Invoice priced in the chain's own crypto; address read from the invoice payment-methods; `token` auth. | +| **xmrcheckout** | `xmrcheckout` | XMR | Same class, different endpoint — the checkout app exposes a **BTCPay-Greenfield-compatible** shim. | +| **wowcheckout** | `wowcheckout` | WOW | Ditto (matched pair to xmrcheckout). | +| **Native Monero** | `native-monero` | XMR, WOW | `src/payments/providers/nativeMoneroProvider.js` — wraps the existing `walletRPCService` (subaddress invoicing + `transfer_split` payouts). | + +The key discovery: **BTCPay, xmrcheckout, and wowcheckout all speak one Greenfield contract**, so a single +`BTCPayProvider` class (constructor-configurable `id`) serves all three — just registered at different +endpoints. See the `btcpay-infra-topology` note for the operator's LAN endpoints. + +## Bootstrap & routing + +`buildProviderRegistry({env, walletService})` (`src/payments/providers/index.js`) is the single wiring seam: + +- Native Monero/Wownero provider registers whenever a `walletService` exists. +- Each Greenfield gateway activates from its own env trio: + `BTCPAY_URL/_STORE_ID/_API_KEY` (chains from `BTCPAY_CHAINS`, default BTC,LTC), + `XMRCHECKOUT_URL/_STORE_ID/_API_KEY` (XMR), `WOWCHECKOUT_URL/_STORE_ID/_API_KEY` (WOW). +- Default routing: BTC/LTC → btcpay, XMR/WOW → their checkout gateway (else native). `PAYMENT_ROUTING` + (`CHAIN:provider,…` or JSON) overrides any mapping. + +**Behavior-preserving:** with no gateway env set (prod today), only the native provider registers and +*every* chain routes to it — byte-for-byte the legacy flow. + +## The hot path + +- **Invoice creation** — `gameModeManager.createPaymentRequest` calls `getProvider(cryptoType).createInvoice(...)`. + The native provider delegates to the same `walletService.createPaymentRequest`, so the subaddress and + monitoring maps are identical to before. +- **Monitoring** — `network/paymentHandlers._monitorAddress` calls `provider.startWatch`. Native passes the + raw wallet status straight through (legacy path unchanged); `BTCPayProvider.getWalletStatus` maps a + Greenfield invoice to the same raw shape (`Settled → confirmed+complete`, `Processing → in_mempool`, + paid/required amounts from payment-methods via `ChainProfile` decimals). The confirmation logic is + extracted to `_handlePaymentStatus`, shared by native + gateway. +- **Payouts** stay native (the checkout apps are receive-only; XMR/WOW payouts remain on wallet-RPC). + +## Turning a gateway on + +1. Mint a store API key in the gateway's admin (money infra — a human step). +2. Set the `*_URL/_STORE_ID/_API_KEY` env on the deploy host, using an address the host can actually reach + (verify LAN reachability; a nebula/overlay IP may not route). +3. Restart. Invoices for that chain now create + confirm through the gateway; no code redeploy. + +## Money math + +All amounts are **atomic BigInt**, decimals-parameterized via `src/money/atomic.js` (`format`, `add`, +`mulByDecimal`, …) and `src/chain/chainProfile.js` (`decimalsFor`, `atomicDivisor`). Never floats. WOW = 11 +decimals, XMR/BTC/LTC per their chain, GRIN = 9. + +## Files + +``` +src/payments/providers/paymentProvider.js contract + registry +src/payments/providers/btcpayProvider.js Greenfield client (btcpay / xmrcheckout / wowcheckout) +src/payments/providers/nativeMoneroProvider.js wraps walletRPCService +src/payments/providers/index.js buildProviderRegistry (env → registry) +src/payments/walletRPCService.js Monero/Wownero wallet-RPC +src/game/gameModeManager.js createPaymentRequest (invoice seam) +src/network/paymentHandlers.js _monitorAddress / _handlePaymentStatus (monitoring seam) +``` diff --git a/docs/RULESETS.md b/docs/RULESETS.md new file mode 100644 index 0000000..e40c9e9 --- /dev/null +++ b/docs/RULESETS.md @@ -0,0 +1,80 @@ +# Rulesets & PvP (P4) + +Gameplay is a **data object**. Instead of hardcoding "first to the exit wins," the match engine reads a +`Ruleset`, so new modes (PvP last-alive, score attack, co-op) are data — not new subsystems. + +## The Ruleset + +`src/game/rulesets/Ruleset.js` — `defineRuleset(spec)` normalizes a partial spec to a frozen, clamped +object: + +``` +id, label, mode +world { difficultyPreset, maps } +entities { monster, monsterCount, pvpCombat } +players { min, max } +winCondition { type } first-to-exit | last-alive | high-score | all-escape +timing { tickMs, minDurationMs, hardCeilingMs, blockDeadline } +economy { model, houseFeePercent, payoutMultipliers } +``` + +`registry.js` ships the built-ins — `solo-classic` (descriptor for the live single-player mode), +`race`, `last-alive`, `score-attack`, `coop-escape` — plus `getRuleset(id)`, `listRulesets()`, and +`rulesetFromMatchOpts()` (maps the legacy MatchRoom option bag to a `race` ruleset, preserving old +behavior). + +## Win conditions as strategies + +`src/game/rulesets/winConditions.js` — each condition is a small strategy the `MatchRoom` consults at +three decision points: + +``` +onExit(room, id) a player reached the exit — end the match? set a winner? +onDeath(room) a player died — is the match over? +rank(room) final ordering (best-first) for placement + winnerId +``` + +- **`FIRST_TO_EXIT`** is a *verbatim port* of the original race logic, so existing matches are + byte-identical. +- **`LAST_ALIVE`** (PvP): reaching the exit means you *survived* (safe, not an instant win); the match + ends when ≤1 contender remains; `rank` orders last-standing → escaped → died-later. Combined with + `entities.pvpCombat`, stepping onto a living rival **strikes them down** (attacker holds position). +- **`HIGH_SCORE`**: no instant win; rank by score at the end. +- **`ALL_ESCAPE`** (co-op): ends when everyone alive has escaped. + +## Where it plugs in + +`MatchRoom` (`src/multiplayer/MatchRoom.js`) resolves a ruleset from `opts.ruleset` / `opts.rulesetId` / +legacy opts, then delegates: `_checkResolution` → `winCondition.onExit`, `_killPlayer` → +`winCondition.onDeath`, `finalize` → `winCondition.rank`. It honors `entities.monster` (optional monster) +and `entities.pvpCombat`, and takes timing from the ruleset. `MatchEngine` is a thin tick driver +(`room.resolveTick()` on an interval). + +**Scope note:** the *live single-player* engine (`src/game/game.js`) is intentionally **not** refactored to +consume rulesets (too risky to touch the deployed path) — `solo-classic` is a catalog descriptor only. PvP +modes are opt-in via `rulesetId`; the match scheduler still builds `race`, and match mode is gated by +`MATCH_ENABLED` (inert by default), so nothing changes until wired. + +## Economy per mode + +A ruleset's `economy` field (`model`, `houseFeePercent`, `payoutMultipliers`) maps onto the existing +`gameModeManager` fields (single-player: multiplier-on-entry-fee) and `matchPayoutService` (PvP: +winner-takes-pot-minus-house). See [MONETIZATION.md](MONETIZATION.md). + +## Pending: block-time-aware difficulty + +`difficultyConfig` presets (easy/normal/hard/casino) tune dungeon size, monster behavior, treasure, and a +target house-win-rate — but currently key off payment mode, **not** mean block time. Coupling difficulty +to `ChainProfile.meanBlockTimeMsFor(chain)` (so Grin ~1 min stays fair vs BTC ~10 min) is a pending +game-balance change, not just wiring. + +## Files + +``` +src/game/rulesets/Ruleset.js defineRuleset + WIN/ECONOMY enums +src/game/rulesets/winConditions.js FIRST_TO_EXIT (race) / LAST_ALIVE / HIGH_SCORE / ALL_ESCAPE +src/game/rulesets/registry.js built-ins + getRuleset/listRulesets/rulesetFromMatchOpts +src/multiplayer/MatchRoom.js consumes the ruleset; PvP combat; delegates win/lose/rank +src/multiplayer/MatchEngine.js tick driver +src/game/difficultyConfig.js world presets (the ruleset's `world` half) +``` diff --git a/src/package.json b/src/package.json index dd85266..0ce623e 100644 --- a/src/package.json +++ b/src/package.json @@ -10,7 +10,8 @@ "test": "jest --runInBand", "db:create": "node scripts/setup_db.js", "db:drop": "node scripts/setup_db.js drop", - "db:reset": "npm run db:drop && npm run db:create" + "db:reset": "npm run db:drop && npm run db:create", + "docs:api": "jsdoc2md --files game/**/*.js network/**/*.js payments/**/*.js multiplayer/*.js chain/*.js utils/*.js money/*.js services/*.js config/*.js > ../docs/API.md" }, "config": { "debug": { @@ -44,6 +45,7 @@ }, "devDependencies": { "jest": "^29.7.0", + "jsdoc-to-markdown": "^9.1.1", "playwright-core": "^1.61.1" } }