Wownerogue
Wownerogue is a browser-based roguelike that synchronizes dungeon runs with Monero (XMR) and Wownero (WOW) block timing. The backend is a Node.js/Express server with Socket.IO for realtime play, optional payment enforcement, and automated payouts.
Overview
- Multiplayer dungeon crawler rendered in the browser
- Three game modes with configurable pricing and payouts
- Optional crypto payments with automatic subaddress management
- Provably fair gaming with pre-game hash commitments
- Live spectator mode - watch any active game in real-time
- Persistent chat with 30-day history for new users
- Configurable difficulty with house edge tuning
- Centralized error handling, rate limiting, and memory management
- Jest coverage for wallet RPC interactions, payment flow, security checks, and game helpers
Difficulty System
Dungeon difficulty is configurable via presets that control dungeon size, monster behavior, and placement:
| Preset | Dungeon Size | Monster Aggression | Target House Win Rate |
|---|---|---|---|
easy |
30×15 | Low (60%) | 30% |
normal |
45×22 | Medium (80%) | 55% |
hard |
55×28 | High (90%) | 65% |
casino |
60×30 | Very High (95%) | 70% |
Set DIFFICULTY_PRESET=casino in .env for paid games. The default automatically selects casino for paid modes and normal for free play.
Provably Fair Gaming
Every game uses cryptographic verification to prove fairness:
- Pre-game: Server generates a random seed and shows its SHA-256 hash to the player
- During game: The seed deterministically generates dungeon layout, positions, etc.
- Post-game: Server reveals the seed so players can verify hash(seed) = pre-game commitment
- Verification: Players can regenerate the dungeon using the seed to confirm fairness
This prevents the server from generating unfair dungeons or changing outcomes after seeing payments.
Spectator Mode
Watch any active game in real-time without joining the queue or paying:
- Click the "👁️ Watch Games" button to see a list of active games
- Each game shows: player ID, duration, move count, treasure status, and spectator count
- Click "Watch Game" to start spectating
- Press ESC or click "Leave Spectate" to exit
Technical details:
- Games list auto-refreshes every 3 seconds
- Spectators receive real-time game state updates via Socket.IO rooms
- Pagination supports 100+ simultaneous games
- Spectators cannot interact with the game (input is disabled)
- Game over events are broadcast to spectators with final status
Chat System
Persistent chat with history for community engagement:
- 30-day message retention in PostgreSQL with automatic cleanup
- New users receive last 50 messages on connect for context
- Rate limiting prevents spam (12 messages per 10 seconds)
- Address detection recognizes XMR/WOW addresses pasted in chat
- Commands:
hello,enter,cancel,confirm,address,pay,stats
Chat messages are stored in the chat_messages table and cached in memory for fast retrieval.
Game Modes
The unified payment config exposes two monetized modes. They map to the legacy identifiers still exposed over the socket and REST APIs.
| Legacy Identifier | Unified Key | Description | Default Cost¹ | Default Payout |
|---|---|---|---|---|
FREE |
n/a | Payments disabled. Runs never require a wallet or address. | n/a | n/a |
PAID_SINGLE |
direct |
Per-run charge. Requires a confirmed payout address unless DIRECT_REQUIRES_ADDRESS=false. |
1 WOW (1e11 atomic) | 2× escape, 3× escape+treasure |
PAID_CREDITS |
credits |
Credit bundles; each run consumes one credit. Mixed mode allowed when ALLOW_MIXED_MODE=true. |
10 credits for 5 WOW (small bundle) | Credits only (payouts disabled) |
¹ Atomic amounts use 11 decimal places (Monero/Wownero standard). Update DIRECT_GAME_PRICE or CREDITS_PACKAGES to change pricing. Config changes take effect when the snapshot refreshes or the process restarts.
Payment System
- Wallet RPC service (
walletRPCService.js): Handles subaddress creation, monitors payments, and batches payouts. Errors are wrapped in typed exceptions for consistent handling. - Game Mode manager (
gameModeManager.js): Applies payment configuration, validates player state, and coordinates payouts. - Payment handlers (
paymentHandlers.js): Manages request lifecycle, socket notifications, and queue integration. - Environment validator (
config/environmentValidator.js): Rejects unsafe payment configs and falls back toFREEmode when necessary. - Address detection (
network/addressManager.js): Parses XMR/WOW addresses shared in chat and requests confirmation before storing them.
If the wallet RPC service or configuration is unavailable the backend automatically downgrades to FREE mode so players can still start games.
Backend Components
src/index.js: Express entry point, REST routes, Socket.IO server, payout scheduler.src/network/: Socket handlers, connection/session management, chat commands, queue logic, rate limiting.src/game/: Dungeon generation, game state, movement, mode management, lighting/FOV helpers.src/payments/: Wallet RPC integration and QR code support.src/db/: PostgreSQL access layer and migration utilities.src/utils/: Shared utilities including memory management and custom error classes.src/middleware/: Express async wrapper and error middleware.
API Surface
Health & Status
GET /health– Server health check with uptime, memory usage, game counts, and wallet status.GET /api/game-modes– Current game modes and pricing configuration.
User Endpoints
GET /api/user/:socketId/credits– Remaining credit balance for a user.GET /api/user/:socketId/mode– User payment mode preferences and enabled features.POST /api/user/:socketId/address– Store payout address (body:{ "address": "<XMR/WOW address>" }).
Payment Endpoints (Socket.IO Preferred)
The following REST endpoints exist but return 501 Not Implemented. Payment flows are handled via Socket.IO events for real-time feedback:
POST /api/payment/create– Use socket eventrequest_paymentinstead.GET /api/payment/status/:paymentId– Use socket eventpayment_statusinstead.POST /api/payment/callback– Reserved for future webhook integration.
Socket.IO Events (Primary Payment Flow)
| Client → Server | Description |
|---|---|
request_payment |
Request a payment address for game entry |
confirm_address |
Confirm a detected payout address |
cancel_address |
Cancel pending address confirmation |
join_queue |
Join the game queue after payment |
| Server → Client | Description |
|---|---|
payment_created |
Payment address and QR code ready |
payment_confirmed |
Payment received and confirmed |
address_detected |
Payout address detected in chat |
address_confirmed |
Payout address saved |
game_start |
Game session beginning |
credits_update |
Credit balance changed |
All gameplay functions work without wallet RPC access; only paid modes require it.
Installation
Prerequisites
- Node.js 16 or newer
- npm
- PostgreSQL 12+
- Optional: Monero/Wownero Wallet RPC endpoints
Steps
- Clone and enter the server directory.
git clone <repository-url> cd wownerogue/src - Install dependencies.
npm install - Configure the environment file.
cp .env.example .env # edit .env to match your database and wallet settings - Provision the database.
# Create database and user npm run db:create # (Optional) To reset the database later: # npm run db:reset - Start the development server.
npm run dev
The default Socket.IO client served from /html listens on port 3000. Configure a reverse proxy if you need TLS or path routing.
Production Deployment
Building for Production
There is no build step required—the Node.js server runs directly. However, ensure you configure the environment correctly:
# Set production environment
export NODE_ENV=production
# Start the production server
npm start
# Or with explicit environment:
NODE_ENV=production node index.js
Production vs Development
| Feature | Development (npm run dev) |
Production (npm start) |
|---|---|---|
NODE_ENV |
development | production |
| Console logging | Verbose | Minimal |
| Error details | Full stack traces | Safe messages only |
| Debug hotkeys | Available | Disabled |
| Simulated blocks | Auto-enabled if no RPC | Requires real daemon |
Reverse Proxy Setup (Nginx)
For TLS termination and serving the static frontend:
upstream wownerogue_backend {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 443 ssl http2;
server_name play.yoursite.com;
ssl_certificate /etc/letsencrypt/live/play.yoursite.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/play.yoursite.com/privkey.pem;
# Static files (game client)
location / {
root /var/www/wownerogue/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# Admin dashboard (restrict access!)
location /admin.html {
# Basic auth or IP whitelist recommended
# auth_basic "Admin";
# auth_basic_user_file /etc/nginx/.htpasswd;
root /var/www/wownerogue/html;
}
# API and Socket.IO
location /socket.io/ {
proxy_pass http://wownerogue_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 86400;
}
location ~ ^/(health|api|verify) {
proxy_pass http://wownerogue_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP redirect
server {
listen 80;
server_name play.yoursite.com;
return 301 https://$server_name$request_uri;
}
Reverse Proxy Setup (Caddy)
Caddy auto-provisions TLS certificates:
play.yoursite.com {
root * /var/www/wownerogue/html
file_server
# Admin dashboard - consider adding basicauth
# basicauth /admin.html {
# admin $2a$14$hashedpassword
# }
# WebSocket and API proxy
@backend {
path /socket.io/* /health /api/* /verify/*
}
reverse_proxy @backend localhost:3000
}
Process Management (systemd)
Create /etc/systemd/system/wownerogue.service:
[Unit]
Description=Wownerogue Game Server
After=network.target postgresql.service
[Service]
Type=simple
User=wownerogue
WorkingDirectory=/var/www/wownerogue/src
EnvironmentFile=/var/www/wownerogue/src/.env
ExecStart=/usr/bin/node index.js
Restart=on-failure
RestartSec=10
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/www/wownerogue/src
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable wownerogue
sudo systemctl start wownerogue
sudo systemctl status wownerogue
Production Checklist
- Set
NODE_ENV=productionin.env - Configure TLS via reverse proxy (Nginx/Caddy)
- Set up PostgreSQL with proper credentials (not default)
- Configure Wownero/Monero wallet-rpc with authentication
- Enable firewall rules (only expose 80/443, not 3000)
- Set up log rotation for application logs
- Configure automated database backups
- Test the full payment flow with real funds
- Protect
/admin.htmlwith authentication - Review and tune rate limits for expected traffic
Configuration
Key environment variables (see .env.example for the full list):
# Network selection (Monero only - Wownero only has mainnet)
MONERO_NETWORK=mainnet # mainnet, stagenet, or testnet
# ⚠️ STAGENET: Stagenet XMR has no value! UI shows warning when not mainnet.
# Difficulty preset
DIFFICULTY_PRESET=casino # easy, normal, hard, casino (auto-selects for paid modes)
# Unified payments
PAYMENTS_ENABLED=true
PAYMENT_MODES=direct,credits
# Direct (per-run) mode
DIRECT_PAYMENT_ENABLED=true
DIRECT_GAME_PRICE=100000000000 # 1 WOW (atomic units)
DIRECT_REQUIRES_ADDRESS=true
DIRECT_ALLOW_GUEST_PLAY=false
DIRECT_PAYOUTS_ENABLED=true
DIRECT_PAYOUT_ESCAPE=2.0
DIRECT_PAYOUT_TREASURE=3.0
# Credits mode
CREDITS_ENABLED=true
CREDITS_PER_GAME=1
CREDITS_REQUIRES_ADDRESS=true
ALLOW_MIXED_MODE=true
PREFER_CREDITS_FIRST=true
CREDITS_PACKAGES='[{"id":"small","credits":10,"price":"500000000000","bonus":0}]'
CREDITS_PAYOUTS_ENABLED=false
CREDITS_PAYOUT_BASE=50000000000
CREDITS_PAYOUT_ESCAPE=1.5
CREDITS_PAYOUT_TREASURE=2.0
# Payout processing
PAYOUTS_ENABLED=true
PAYOUT_MIN_AMOUNT=10000000000
PAYOUT_MAX_PER_GAME=10000000000000
PAYOUT_BATCH_INTERVAL=300
PAYOUT_MAX_RETRIES=3
MAX_PAYOUT_BATCH_SIZE=50
# Limits
MAX_GAMES_PER_HOUR=60
MAX_PAYOUTS_PER_DAY=100
MAX_CREDIT_PURCHASE_PER_DAY=100000000000000
GAME_COOLDOWN_SECONDS=5
# Legacy override (optional)
GAME_MODE=
SINGLE_GAME_PRICE=100000000000
CREDITS_PACKAGE_PRICE=500000000000
# Wallet RPC
PRIMARY_WALLET_ENDPOINT=http://127.0.0.1:34570
# WALLET_RPC_USER=<rpc-user>
# WALLET_RPC_PASSWORD=<rpc-password>
# Blockchain RPC
PRIMARY_RPC_ENDPOINT=http://127.0.0.1:34568
FALLBACK_RPC_ENDPOINT=http://127.0.0.1:34568
RPC_POLL_INTERVAL=2000
# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=wownerogue
DB_USER=jw
DB_PASSWORD=jw
# Server
PORT=3000
NODE_ENV=development
# Block timing overrides
BLOCK_SOURCE=daemon
# SIMULATED_BLOCKS=false
# FORCE_SIMULATED_BLOCKS=false
- Amounts are specified in atomic units (1 WOW = 1e11) unless otherwise noted.
- Leave wallet credentials unset to enforce
FREEmode; payments require RPC access. CREDITS_PACKAGESmust be valid JSON encoded as a single-line string. Adjust the bundle list to match your pricing model.- The legacy
GAME_MODE,SINGLE_GAME_PRICE, andCREDITS_PACKAGE_PRICEvariables remain for compatibility but are ignored whenPAYMENT_MODESis set.
Payment Flow Summary
PAID_SINGLE
- Player requests a paid run.
- Server creates a subaddress and returns the payment details.
- Wallet RPC monitors the subaddress and signals confirmation.
- Game starts; payouts run after successful completion.
PAID_CREDITS
- Player buys a ten-run bundle.
- Credits are recorded in the database on confirmation.
- Each game consumes one credit; no automatic payouts occur.
Address confirmation
- Player pastes an address in chat.
- Regex detection flags the message and issues a warning.
- Player confirms; the address is persisted for payouts.
Session Persistence
Player sessions are tied to an anonymous token stored in the browser's localStorage. This token persists:
- ✅ Across page refreshes – Reconnecting reloads your credits and payout address.
- ✅ After closing the browser – The token survives normal browser sessions.
- ✅ Server restarts – Session data is stored in PostgreSQL, not memory.
Sessions are lost when:
- ⚠️ Clearing cookies or localStorage
- ⚠️ Using private/incognito browsing mode (localStorage may be disabled or ephemeral)
- ⚠️ Switching browsers or devices
Important notes for players:
- Your payout address and credit balance are tied to your session token.
- If localStorage is unavailable (private browsing), a warning appears next to the "Manage Payout Address" button.
- There is no account system—your session token is your identity. Treat it carefully.
For deployment:
- Ensure your reverse proxy preserves WebSocket connections for token handshake.
- The
wownerogue_tokenkey in localStorage contains the resume token. - Session data (credits, address) is stored in the
userstable with the token inanon_token.
Testing
Run the Jest suite from src:
npm test
Tests cover payment handlers, wallet RPC error propagation, security rules, movement logic, and integration flows. Additional manual scripts live under test/ for browser debugging and lighting verification.
Monitoring and Operations
- Rate limiting metrics and in-memory cleanup stats are exposed through server logs and debug views.
- Optional health endpoints provide game counts, queue length, and rate limiter state.
- Structured error handling ensures failed wallet or RPC calls return consistent payloads and are logged with context.
Socket.IO Events
Client → Server
| Event | Payload | Description |
|---|---|---|
chat |
{ id, message } |
Send chat message or command |
new game |
– | Request to join queue / start game |
cancel queue |
– | Leave the waiting queue |
movement |
{ direction } |
Player movement (0–3 for N/E/S/W) |
get_active_games |
{ page?, limit? } |
Request paginated list of active games |
spectate_game |
{ gameId } |
Start spectating a specific game |
leave_spectate |
– | Stop spectating current game |
Server → Client
| Event | Payload | Description |
|---|---|---|
chat message |
{ id, message, timestamp } |
Broadcast chat message |
chat_history |
{ messages: [...] } |
Last 50 messages on connect |
queue update |
{ position, total } |
Queue position notification |
game started |
{ gameState } |
Game initialization data |
game update |
{ tiles, player, monsters, ... } |
Game state after movement |
game over |
{ reason, stats } |
Game ended (death/escape/treasure) |
active_games |
{ games, total, page, limit } |
Paginated active games list |
spectate_start |
{ gameId, state } |
Initial state when spectating begins |
spectator_update |
{ gameId, state } |
Real-time game state for spectators |
spectate_ended |
{ gameId, reason } |
Game ended while spectating |
Repository Layout
src/
├── index.js # Entry point, server setup
├── config/
│ ├── environmentValidator.js
│ └── paymentConfig.js
├── db/
│ ├── databaseManager.js
│ ├── dbcalls.js
│ └── user.js
├── game/
│ ├── dungeon.js
│ ├── game.js
│ ├── gameManager.js
│ ├── gameModeManager.js
│ ├── lightingAndFov.js
│ ├── monster.js
│ ├── movementManager.js
│ └── player.js
├── network/
│ ├── addressManager.js
│ ├── broadcastManager.js
│ ├── chatHandler.js
│ ├── chatHistoryManager.js # NEW: Persistent chat storage
│ ├── connectionHandler.js
│ ├── paymentHandlers.js
│ ├── queueHandler.js
│ ├── queueManager.js
│ ├── rateLimiter.js
│ ├── sessionManager.js
│ ├── socketHandlers.js
│ └── spectatorManager.js # NEW: Live game spectating
├── payments/
│ ├── moneroPayService.js
│ ├── qrService.js
│ └── walletRPCService.js
├── rpc/
│ └── rpcService.js
└── utils/
├── errors.js
└── memoryManager.js
html/
├── index.html
├── admin.html
└── js/
├── core/
├── display/
├── input/
├── network/
└── ui/
test/
└── *.test.js
Refer to REFACTORING_SUMMARY.md for module-by-module details and migration history.
Architecture Highlights
Spectator System:
- Uses Socket.IO rooms (
spectate:{gameId},lobby) for efficient broadcasting - Game list cached for 2 seconds to handle 100+ concurrent games
- State broadcasts every 3 seconds plus on every move
- Spectators receive
game updateevents identical to players
Chat History:
- PostgreSQL
chat_messagestable with 30-day retention - In-memory cache for fast retrieval
- Automatic cleanup of old messages on startup
Rate Limiting:
- Per-IP sliding window for chat (12 messages/10s)
- Connection rate limits with configurable thresholds
- Queue position spam prevention
Next Steps
- ✅ Implement spectator mode for live game watching
- ✅ Add persistent chat history for new users
- Run full payment integration test with real wallet-rpc
- Configure automated PostgreSQL backups
- Set up log rotation and monitoring alerts
- Load test rate limiting under production traffic patterns