Files
wownerogue/README.md
T

503 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Wownerogue
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/race rulesets, with separate free and paid
leaderboards.
## Such Software-operated products
Such Software (`apps@such.software`) operates only these two public products:
| Service | Operated scope |
|---|---|
| `play.wowne.ro` | Wownero mainnet free play and pay-for-credits leaderboard/prestige play. Credits are non-redeemable; there are no prizes, cash-out, or payouts, and the service is not offered or marketed as gambling. Legal classification depends on applicable law. |
| `monerogue.app` | Monero **stagenet only**, with direct solo entry only. Single-player 2×/3× outcomes are test gambling mechanics using test coins with **NO REAL VALUE**. Purchased-credit entry and crypto-match payouts are off. Never send mainnet XMR. |
The repository is self-hostable under the MIT License. A third-party operator is solely responsible
for its deployment, product, legal compliance, funds, players, claims, and support. Such Software
does not operate, supervise, endorse, or accept responsibility for a third-party deployment.
## Features
- **Provably fair solo runs** with two-party, pre-entry seed commitments and per-depth layout
fingerprints
- **Multi-level descent** whose depth scales with the chain's block time
- **Live spectator mode**: watch active games in real time
- **Persistent chat** with a rolling 7-day server-side history
- **Transaction history**: payment and payout records per session
- **Multiple entry economies**: free play, per-run chain payment, or purchased credit bundles
- **Anonymous social Tavern** (no account required) with chat, live solo spectating, and in-place
race queues
- **Shared render kit** with Tiled, ASCII, Iso, and 3D techniques across the dungeon and Tavern,
driven by an operator-owned cosmetic catalog
- **Real-time multiplayer races** (optional): free, prestige-credit, or crypto winner-take-pot
races on a per-block cadence
- **Wallet RPC integration** for payment address issuance, payment detection, and batched payouts
## Requirements
- Node.js >= 22, npm >= 10
- PostgreSQL
- A Wownero or Monero wallet RPC endpoint (only required for paid modes)
## Quick Start
```bash
git clone https://github.com/Such-Software/wownerogue.git
cd wownerogue/src
npm install
cp .env.example .env # Edit with your database/wallet settings
npm run db:create # Initialize PostgreSQL
npm run dev # Start development server
```
Open http://localhost:3000 to play. `src/.env.example` is the full, annotated environment
template; the file at the repository root is a two-flag stub, not a starting point.
---
## Game Modes
`GAME_MODE` selects the solo entry economy for an instance.
| Mode | Cost | Payout | Leaderboard |
|------|------|--------|-------------|
| `FREE` | None | None | Pleb |
| `PAID_SINGLE` | Per-run chain payment | Profile-controlled | Paid prestige |
| `PAID_CREDITS` | Purchased credit | Profile-controlled | Paid prestige |
The Such Software production profiles are `src/.env.mainnet.example` (credits only, every payout
path off) and `src/.env.stagenet.example` (direct-entry-only stagenet 2×/3× solo test payouts,
purchased credits and crypto-match payouts off). Each opts into a startup contract with
`OPERATED_PRODUCT_PROFILE`; preflight refuses network, operator-identity, or economic-scope drift.
Generic self-hosts leave that variable unset and are not constrained by those product decisions.
## Match Mode (Multiplayer Races)
Real-time multiplayer races run when `MATCH_ENABLED=true`. A new race starts every crypto block as
long as at least two players are queued. Supported economies:
| Economy | Entry | Payout | Leaderboard |
|---|---|---|---|
| Free | None | None | Pleb |
| Prestige Credits | Credits | None | Prestige |
| Crypto Race | Race-entry ticket | Winner-take-pot minus house fee | Hall of Champions |
`MATCH_ENABLED` gates the creation of new rooms only; settlement reconciliation for existing rooms
runs regardless, so disabling it cannot strand an in-flight match. The Tavern's in-place race queue
additionally requires `TAVERN_ENABLED=true`.
See [`docs/MATCH_MODE.md`](docs/MATCH_MODE.md) for architecture, configuration, and API details, and
[`docs/RULESETS.md`](docs/RULESETS.md) for the Escape Race, Last Alive, Score Attack, and Co-op
Escape rulesets.
The [showcase qualification guide](docs/SHOWCASE_QUALIFICATION.md) defines the Tavern, graphics,
Escape Race, and Smirk acceptance checks.
For deterministic multi-bot playtesting and 16:9/9:16 ad footage, use the loopback-only, free-only
capture harness documented in [`docs/PVP_CAPTURE.md`](docs/PVP_CAPTURE.md). It drives the real match
engine over Socket.IO but cannot connect to a deployment, database, wallet, or payout path.
## Difficulty and Pacing
Difficulty has two levers, both keyed to the crypto network, so `cryptoType` shapes the game.
**Presets** (`DIFFICULTY_PRESET`) set the per-level shape:
| Preset | Dungeon Size | Monster | Target House Win |
|--------|--------------|---------|------------------|
| `easy` | 30×15 | low | ~30% |
| `normal` | 45×22 | medium | ~55% |
| `hard` | 55×28 | high | ~65% |
| `casino` | 70×35 | very high | ~70% |
An instance with `GAME_MODE=FREE` and payments off defaults to `normal`; paid modes default to
`casino`. Every knob is env-overridable (`DUNGEON_WIDTH`, `DUNGEON_HEIGHT`, `MONSTER_SPEED`, and so
on).
**Multi-level depth (`levels`)** is the pacing lever and scales with block time. A run descends N
levels, each a preset-sized dungeon with a fair monster. A non-final exit takes the stairs down;
only the final exit escapes, and the treasure sits in the vault on the final level. Because total
run length scales with the chain's block time, the block timer supplies the house edge without a
giant map or a monster that moves faster than the player.
| Chain | Mean block time | Levels |
|-------|-----------------|--------|
| GRIN | ~1 min | 1 |
| XMR | ~2 min | 1 |
| LTC | ~2.5 min | 2 |
| WOW | ~5 min | 2 |
| BTC | ~10 min | 3 |
Level counts live in `NETWORK_TUNING` (`src/game/difficultyConfig.js`). Precedence, low to high:
preset, per-network tuning, env vars, explicit call-site overrides. Override the count with
`DUNGEON_LEVELS`; disable per-network tuning entirely with `NETWORK_TUNING_DISABLED=true`.
Wownero's mean block time is ~5 minutes measured against the live daemon, which places it closer to
Bitcoin than to Monero for calibration purposes. Mean block times per chain are declared once in
`src/chain/chainProfile.js`. See [`docs/BALANCE_SIM.md`](docs/BALANCE_SIM.md) for how balance is
measured and calibrated, and [`docs/MULTI_LEVEL.md`](docs/MULTI_LEVEL.md) for the descent mechanic.
---
## Chain Profiles
`src/chain/chainProfile.js` is the single source for everything chain-specific that is not an RPC
call: decimals and atomic divisor, mean block time, adapter family, and payment URI scheme.
| Chain | Decimals | Mean block time | Family | URI scheme |
|-------|----------|-----------------|--------|------------|
| WOW | 11 | 300000 ms | monero | `wownero` |
| XMR | 12 | 120000 ms | monero | `monero` |
| BTC | 8 | 600000 ms | utxo | `bitcoin` |
| LTC | 8 | 150000 ms | utxo | `litecoin` |
| GRIN | 9 | 60000 ms | mimblewimble | `grin` |
An unrecognized `CRYPTO_TYPE` resolves to a fallback profile shaped like XMR (12 decimals, 120000 ms
mean block time, monero family) so callers never crash on a mis-set value. Adding a chain means one
entry here plus a family adapter.
Note that WOW uses 11 decimals, not Monero's 12: 1 WOW = 10^11 atomic units.
---
## Provably Fair Gaming
1. The server publishes a one-use, socket-bound SHA-256 commitment to a secret server seed before
the client chooses its contribution. Issuance is separate from consumption, so the server cannot
grind its seed after seeing the player's.
2. The browser adds an independent WebCrypto client seed. Neither side alone selects the final
seed.
3. The effective seed is `HMAC-SHA256(serverSeed, clientSeed)` and deterministically generates each
dungeon depth along with its audit fingerprint.
4. After completion, the server seed and the per-depth layout manifest are revealed at
`/verify/:gameId` and `/api/verify/:gameId`. Server seeds for active games stay private.
The proof covers layout generation. Outcome execution is a separate server-authoritative concern:
without a signed input/event transcript, the proof does not independently replay block timing,
player moves, monster turns, the declared result, or payout delivery.
## Spectator Mode
The **WATCH GAMES** control lists active games. Spectators receive real-time updates via Socket.IO
rooms. Press ESC to exit.
## Transaction History
The **History** control shows the session's payment and payout records: total received from payouts,
payment history with status and credits received, and payout history with multipliers and
transaction hashes.
## Session Persistence
Sessions, including anonymous Tavern entry, use an anonymous token stored in `localStorage`:
- Persists across refreshes, browser closes, and server restarts.
- Lost when clearing cookies or `localStorage`, using incognito mode, or switching browsers or
devices.
A warning appears if `localStorage` is unavailable.
## Rendering and Cosmetics
The shared render kit provides four techniques: Tiled, ASCII, Iso, and 3D. Three.js is served from
the application's own origin under a versioned, immutable `/vendor/three/<version>` path, which
keeps it inside the production CSP. Setting `RENDERER_CDN_ENABLED=true` opts into third-party CDN
delivery instead and logs a warning in production.
Cosmetic packs are gated per pack, not by a blanket "has paid" flag. Each catalog entry carries a
tier and an optional lifetime-credits-purchased threshold, and a pack unlocks when either the user's
subscription tier reaches the pack's tier or their cumulative purchased credits reach its threshold.
Purchased credits are not deducted by unlocking. The default ladder is `original` (free), then
thresholds at 1, 5, 10, 20, 40, and 50 lifetime credits. In production the catalog is owned by the
`cosmetic_catalog` database table; the object in `src/multiplayer/entitlements.js` is the fallback
used when that table is unavailable.
See [`docs/RENDER_PACKS.md`](docs/RENDER_PACKS.md) and
[`docs/MONETIZATION.md`](docs/MONETIZATION.md).
---
## API Reference
### Public Endpoints
| Endpoint | Description |
|----------|-------------|
| `GET /health` | Redacted dependency/readiness summary |
| `GET /health/live` | Process liveness probe |
| `GET /health/ready` | Dependency readiness probe (503 while degraded) |
| `GET /api/game-modes` | Current modes and pricing |
| `GET /api/stats` | Public activity counters (short-lived cache, live online count) |
| `GET /api/disclosures` | Active commerce disclosure text for the running profile |
| `GET /api/leaderboard?board=pleb\|champions\|prestige&period=all\|week\|month` | One explicitly separated score board. An omitted `board` means `pleb`; legacy `all`, blank, and unknown board values return 400. An unknown `period` falls back to `all`. |
| `GET /api/user/:socketId/credits` | Credit balance |
| `GET /api/user/:socketId/mode` | Resolved entry mode for the user |
| `GET /api/user/:socketId/payment-options` | Available entry/payment options |
| `GET /api/user/:socketId/payments` | Payment history (paginated) |
| `GET /api/user/:socketId/payouts` | Payout history (paginated) |
| `POST /api/user/:socketId/address` | Set payout address |
| `POST /api/payment/create` | Create a payment request |
| `GET /api/payment/status/:paymentId` | Payment status |
| `GET /verify/:gameId` | Human-readable fairness verification page |
| `GET /api/verify/:gameId` | Machine-readable fairness proof |
| `GET /api/verify?serverSeed=&clientSeed=&effectiveSeed=&commitment=` | Stateless proof check for values you already hold |
`/`, `/tavern`, `/admin`, `/terms`, `/privacy`, and `/responsible-play` serve pages rather than JSON.
### Admin Endpoints
Require an `X-Admin-Key` header matching the `ADMIN_API_KEY` environment variable.
| Endpoint | Description |
|----------|-------------|
| `POST /api/admin/refund/payment` | Request an idempotent refund; consumed grants require review |
| `POST /api/admin/credits/adjust` | Add or remove credits from a user |
| `POST /api/admin/payouts/:id/retry` | Retry a failed payout |
| `GET /api/admin/users` | List users |
| `GET /api/admin/users/search` | Search users by socket ID or address |
| `GET /api/admin/users/:id` | User detail |
| `POST /api/admin/users/:id/chat-ban` | Apply or lift a chat ban |
| `GET /api/admin/chat` | Recent chat messages |
| `DELETE /api/admin/chat/:id` | Remove a chat message |
| `GET /api/admin/queue` | Inspect the entry queue |
| `POST /api/admin/queue/remove` | Remove an entry from the queue |
| `GET /api/admin/stats/overview` | Aggregate service stats |
| `GET /api/admin/stats/games` | Game stats |
| `GET /api/admin/stats/payouts` | Payout stats |
| `GET /api/admin/stats/refunds` | Refund stats |
| `POST /api/admin/alerts/test-email` | Send a test alert email |
**Example: refund a payment**
```bash
curl -X POST http://localhost:3000/api/admin/refund/payment \
-H "Content-Type: application/json" \
-H "X-Admin-Key: your-admin-key" \
-d '{"paymentId": 123, "reason": "Customer request", "sendFunds": true}'
```
**Example: adjust credits**
```bash
curl -X POST http://localhost:3000/api/admin/credits/adjust \
-H "Content-Type: application/json" \
-H "X-Admin-Key: your-admin-key" \
-d '{"socketId": "abc123", "amount": 5, "reason": "Compensation"}'
```
`amount` must be a non-zero safe integer in [-1000000, 1000000]; negative adjustments are applied
atomically and cannot drive a balance below zero.
### Socket.IO Events
**Client to server**
| Event | Description |
|-------|-------------|
| `register_client` | Bind the socket to a session token and receive identity/state |
| `fairness_offer_request` | Request a fresh server-seed commitment |
| `auto_start` | Start a run, using credits when available |
| `play_free` | Start a free run |
| `join_queue` | Join the next-block entry queue |
| `early_entry` | Enter the current block instead of waiting for the next |
| `player_move` | Move the player (direction 0-3) |
| `request_payment` | Request a payment address |
| `check_payment_status` | Poll a pending payment |
| `get_user_credits` | Request the current credit balance |
| `get_active_games` | List games available for spectating |
| `spectate_game` / `leave_spectate` | Enter and leave spectator mode |
| `chat_signed` / `chat_game_queue` | Send a signed chat message; queue chat-driven entry |
| `tavern_join` / `tavern_leave` / `tavern_move` / `tavern_chat` / `tavern_match_list` | Tavern presence, movement, chat, and race listing |
| `match_queue` / `match_move` / `match_leave` / `match_reconnect` | Multiplayer race queue and play |
**Server to client**
| Event | Description |
|-------|-------------|
| `socket_registered` / `session_token` / `session_resumed` | Session lifecycle |
| `fairness_offer` / `fairness_error` | Seed commitment issuance |
| `game_start` | Game initialized with dungeon state |
| `game_update` | State after movement, including `depth` and `maxDepth` for multi-level descent |
| `game_event` | Discrete in-run event: `descend`, `treasure_found`, `escaped`, `monster_caught` |
| `game_over` | Run ended |
| `game_settlement_pending` | Result recorded, settlement still in flight |
| `payment_created` / `payment_detected` / `payment_confirmed` / `payment_underpaid` / `payment_error` | Payment lifecycle |
| `credits_update` | Credit balance changed |
| `spectate_start` / `spectator_update` / `spectate_ended` | Spectator lifecycle |
| `chat_history` / `message` | Chat backlog and live messages |
| `leaderboard_update` / `win_feed` | Leaderboard and win ticker |
| `tavern_joined` / `tavern_update` / `tavern_match_tick` / `tavern_match_end` | Tavern state |
| `match_joined` / `match_end` / `match_settlement_pending` / `match_error` | Race lifecycle |
The client handles `descend` and `treasure_found` from `game_event` with an on-screen banner;
`escaped` and `monster_caught` are conveyed through `game_over`.
---
## Chat
Chat messages are persisted to the `chat_messages` table. A cleanup pass runs once per day and
deletes messages older than 7 days. Clients receive the most recent 50 messages on join. When no
database is available, chat falls back to a bounded in-memory buffer.
The Tavern prefers a shared global chat provider when one is injected. In that configuration a
Tavern message reaches every connected client, not just the Tavern room, and it is persisted along
with the rest of global chat. Only when no global provider is present does the Tavern fall back to
room-scoped, non-persisted chat.
See [`docs/CHAT_AND_NOSTR.md`](docs/CHAT_AND_NOSTR.md) for message signing and the optional nostr
relay path.
---
## Configuration
Key environment variables for a generic self-host. `src/.env.example` is the complete annotated
list.
```bash
# Core
NODE_ENV=production
PORT=3000
CRYPTO_TYPE=WOW
DIFFICULTY_PRESET=casino
# Payments
PAYMENTS_ENABLED=true
PAYMENT_MODES=direct,credits
DIRECT_GAME_PRICE=100000000000 # 1 WOW in atomic units
CREDITS_PACKAGES='[{"id":"small","credits":10,"price":"900000000000","bonus":0}]'
# Payouts
PAYOUTS_ENABLED=false
DIRECT_PAYOUTS_ENABLED=false
DIRECT_PAYOUT_ESCAPE=2.0
DIRECT_PAYOUT_TREASURE=3.0
# Wallet RPC (required for paid modes)
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570
WALLET_RPC_USER=user
WALLET_RPC_PASSWORD=use-a-distinct-strong-rpc-secret
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=wownerogue
DB_USER=your_user
DB_PASSWORD=your_password
# Admin API (generate with: openssl rand -hex 32)
ADMIN_API_KEY=your-secure-key
# Optional modes
TAVERN_ENABLED=true
MATCH_ENABLED=false
# Network selection (Monero only; Wownero has mainnet only)
MONERO_NETWORK=mainnet
```
Amounts are always atomic units for the configured chain. See the chain profile table above for
per-chain decimals.
`npm run preflight` validates the environment against the configured product profile without
starting the service.
---
## Production Deployment
See [`docs/DEPLOY.md`](docs/DEPLOY.md) for production deployment, including:
- systemd service configuration
- Nginx or other reverse proxy setup with WebSocket support
- Database permissions and security hardening
[`docs/DEPLOY_INSTANCES.md`](docs/DEPLOY_INSTANCES.md) covers running multiple instances, such as
Wownero and Monero, on one server.
---
## Operations
See [`docs/LOGS_AND_BACKUP.md`](docs/LOGS_AND_BACKUP.md) for log management with systemd/journald,
automated PostgreSQL backups, and backup verification.
`scripts/deploy/wowngeon-db-backup.sh` writes a dump and then prunes dumps older than
`BACKUP_RETENTION_DAYS` (default 14), plus interrupted temporary files older than one day. It
rejects a non-positive or non-numeric retention value rather than defaulting silently.
Financial operations are documented in
[`docs/FINANCIAL_RECONCILIATION.md`](docs/FINANCIAL_RECONCILIATION.md),
[`docs/FINANCIAL_CONSTRAINT_VALIDATION.md`](docs/FINANCIAL_CONSTRAINT_VALIDATION.md),
[`docs/FINANCIAL_EVENT_EXPORT.md`](docs/FINANCIAL_EVENT_EXPORT.md), and
[`docs/STAGENET_FINANCIAL_CANARY.md`](docs/STAGENET_FINANCIAL_CANARY.md).
---
## Testing
```bash
cd src
npm test
```
Tests cover payment handlers, wallet RPC, security rules, movement logic, fairness verification, and
integration flows.
---
## Project Structure
```
src/
├── index.js # Entry point, HTTP routes, static and vendor asset serving
├── auth/ # Session and wallet-based authentication
├── chain/ # Chain profile registry
├── config/ # Payment config, environment validation, product profiles
├── db/ # PostgreSQL layer
├── game/ # Dungeon generation, player, monster, fairness, mode resolution
├── middleware/ # Express middleware
├── migrations/ # Numbered SQL migrations
├── money/ # Atomic-unit arithmetic
├── multiplayer/ # Entitlements, appearance, cosmetic catalog
├── network/ # Socket handlers, chat, queue, spectator, match, Tavern
├── payments/ # Wallet RPC, QR codes
├── routes/ # Admin, auth, and leaderboard routers
├── rpc/ # Daemon and wallet clients
├── services/ # Health, alerts, refunds, payouts, subscriptions, exports
├── sim/ # Balance simulation harness
├── utils/ # Errors, memory management, nostr helpers
└── views/ # Server-rendered legal and verification pages
html/
├── index.html # Game client
├── tavern.html # Tavern client
├── match.html # Match client
├── admin.html # Admin dashboard
├── assets/ # Render packs and images
└── js/ # Frontend modules (core, render, network, ui, input)
test/
└── *.test.js # Jest tests
docs/ # Architecture, deployment, payments, and design references
scripts/ # Deployment units, backup, asset build, wallet maintenance
```
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) describes how these pieces fit together.
---
## License
The source is available under the [MIT License](LICENSE). MIT permission to use, copy, modify,
merge, publish, distribute, sublicense, and sell copies is subject to including the copyright and
permission notice in all copies or substantial portions. The software is provided "AS IS", without
warranty of any kind, as stated in the License.
The MIT License governs rights in the source. Documentation, examples, and product disclosures are
informational only; they are not legal advice or a determination that a deployment complies with
applicable law. Such Software operates only the two products and scopes listed above. The MIT
License does not make an independent deployment a Such Software service or authorize it to claim
Such Software sponsorship or endorsement; its operator remains solely responsible for the service.