mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-25 17:18:33 +01:00
feat: real-time multiplayer race mode (free, prestige, crypto)
Adds a complete block-cadence multiplayer race mode alongside solo and tavern.
Disabled by default; enable with MATCH_ENABLED=true.
Highlights:
- New shared engine: MatchRoom, MatchEngine, MatchState (transport-agnostic, unit-tested).
- Per-economy queues: free, credits_prestige, crypto_race with atomic escrow/refund.
- MatchScheduler drains queues on every new block and starts 2–32 player races.
- MatchManager handles Socket.IO lifecycle, server-authoritative movement, reconnect grace,
forfeit/AFK handling, and end-of-match persistence.
- Crypto races use race-entry tickets (grants.race_entries) to avoid on-chain refunds; winner
payout is queued through the existing batch payout/retry service.
- Leaderboard integration: free → Pleb, prestige → /api/leaderboard?board=prestige,
crypto → Hall of Champions.
- Tavern integration: Queue for Race panel, active race list, and live spectator rendering.
- Dedicated /match.html client with economy selector, race HUD, and post-match scoreboard.
- Migration 022 adds matches, match_entrants, match_events, match_queue_entries,
race_entry_transactions, users.race_entries, and payouts.match_id.
Docs:
- New docs/MATCH_MODE.md with full architecture, config, events, and operational notes.
- Updated README.md, docs/TAVERN_AND_MULTIPLAYER.md, docs/TODO.md, src/.env.example,
and src/.env.
Tests: 40 new unit tests across 7 suites, all passing.
Known follow-ups for real-world validation:
- Full integration test with Postgres + Socket.IO.
- Server boot check with MATCH_ENABLED=false (default) to ensure solo/tavern remain unaffected.
- Browser render-kit verification in /match.html and tavern spectator.
- Crypto-race payout end-to-end with real wallet-rpc.
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# Match Mode — Race/PvP Multiplayer
|
||||
|
||||
> Status: **Implemented** — all five milestones complete. Single-player and Tavern modes are untouched.
|
||||
|
||||
Match mode adds real-time multiplayer races to Wownerogue. It reuses the existing shared-world
|
||||
engine (`Room`/`Occupant`), the render kit, the payment/payout systems, and provably-fair seeding.
|
||||
|
||||
## Supported economies
|
||||
|
||||
| Economy | Entry cost | Payout | Leaderboard |
|
||||
|---|---|---|---|
|
||||
| `free` | None | None | Pleb board |
|
||||
| `credits_prestige` | Credits | None (prestige only) | Prestige board |
|
||||
| `crypto_race` | Race-entry ticket | Winner-take-pot (minus house fee) | Hall of Champions |
|
||||
|
||||
Unredeemable credits are the same `users.credits` balance running with `CREDITS_PAYOUTS_ENABLED=false`;
|
||||
there is no separate "fake credit" system. Crypto races use non-refundable **race-entry tickets**
|
||||
(`users.race_entries`) so players never need an on-chain refund when leaving a queue before a match
|
||||
starts.
|
||||
|
||||
## Lifecycle: a race every block
|
||||
|
||||
The existing solo `queueHandler` already reacts to new crypto blocks. `MatchScheduler` subscribes to
|
||||
the same block event and drains per-economy queues:
|
||||
|
||||
- If a queue has ≥2 players at block H, all queued players are placed into a new `MatchRoom` and the
|
||||
race starts immediately.
|
||||
- If a queue has 1 player, they carry over to block H+1 and may leave at any time.
|
||||
- If a queue is empty, nothing happens.
|
||||
|
||||
A race ends when:
|
||||
1. The first player reaches the exit.
|
||||
2. The next block is detected (with a `MATCH_MIN_DURATION_MS` floor to prevent degenerate short
|
||||
races).
|
||||
3. The `MATCH_HARD_CEILING_MS` absolute ceiling expires.
|
||||
|
||||
If no one escaped, living players are ranked by proximity to the exit; dead players are ranked after
|
||||
them by progress, treasure, and moves.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Engine layer (`src/multiplayer/`)
|
||||
|
||||
- `MatchRoom.js` — extends `Room`. Owns a shared deterministic dungeon, shared monster, player
|
||||
life/death/finish state, treasure, and the block/timer deadline. Serves as the synchronous tick
|
||||
resolver.
|
||||
- `MatchEngine.js` — server tick driver. Wraps a `MatchRoom` with a `setInterval` timer, calls
|
||||
`resolveTick()`, and fires `onFinish` when the match ends.
|
||||
- `MatchState.js` — serialization helpers that produce broadcast/persistence shapes, including the
|
||||
renderer-agnostic `gameState` consumed by `html/js/render/sceneModel.js`.
|
||||
|
||||
### Transport / lifecycle layer (`src/network/`)
|
||||
|
||||
- `matchQueue.js` — persisted per-economy queues (`free`, `credits_prestige`, `crypto_race`),
|
||||
join/leave, atomic credit/ticket escrow.
|
||||
- `matchScheduler.js` — block-cadence scheduler that drains queues into matches.
|
||||
- `matchManager.js` — Socket.IO rooms, match lifecycle, broadcasts, reconnect grace, persistence.
|
||||
- `matchPayoutService.js` — crypto-race pot accounting, house-fee split, winner payout record.
|
||||
- `matchLeaderboard.js` — posts match scores to the correct leaderboard.
|
||||
- `tavernMatchBridge.js` — forwards public match state to the tavern room for spectators.
|
||||
|
||||
### Persistence
|
||||
|
||||
See migration `src/migrations/022_match_mode.sql`:
|
||||
|
||||
- `matches` — one race, provably-fair seed, economy, pot/fee, winner.
|
||||
- `match_entrants` — per-player state, placement, score, payment link.
|
||||
- `match_events` — replay / spectator / audit feed.
|
||||
- `match_queue_entries` — persisted queue for restart safety.
|
||||
- `race_entry_transactions` — race-entry ticket ledger.
|
||||
- `users.race_entries` — hot-read ticket balance.
|
||||
- `payouts.match_id` — links winner payout to match.
|
||||
|
||||
### Provably fair
|
||||
|
||||
Each match commits to a SHA-256 `seed_hash` at creation. The `seed` is revealed on finish so anyone
|
||||
can regenerate the exact dungeon and verify the hash. This mirrors the existing solo
|
||||
`games.dungeon_seed` / `games.seed` pattern.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to `src/.env` (documented in `src/.env.example`):
|
||||
|
||||
```bash
|
||||
# Enable match mode (default false)
|
||||
MATCH_ENABLED=false
|
||||
|
||||
# Max players per race (2–32)
|
||||
MATCH_MAX_PLAYERS=4
|
||||
|
||||
# Server tick interval in ms
|
||||
MATCH_TICK_MS=250
|
||||
|
||||
# Minimum race duration before the next block can end it
|
||||
MATCH_MIN_DURATION_MS=20000
|
||||
|
||||
# Hard ceiling after which a race is force-ended
|
||||
MATCH_HARD_CEILING_MS=240000
|
||||
|
||||
# Credits cost for prestige-only races
|
||||
MATCH_CREDITS_COST=1
|
||||
|
||||
# Crypto race house fee percent (default 5)
|
||||
MATCH_HOUSE_FEE_PERCENT=5
|
||||
|
||||
# Optional: override per-player entry fee atomic amount.
|
||||
# If unset, uses the existing per-game price (singleGamePrice / DIRECT_GAME_PRICE).
|
||||
# MATCH_ENTRY_FEE_ATOMIC=5000000000
|
||||
|
||||
# Enable the crypto race-entry ticket economy
|
||||
MATCH_CRYPTO_RACE_ENABLED=false
|
||||
```
|
||||
|
||||
Crypto races also require:
|
||||
- A paid instance (`GAME_MODE=PAID_SINGLE` / `PAID_CREDITS` or equivalent `PAYMENT_MODES`).
|
||||
- Race-entry ticket products in the credit/cosmetic catalog with `grants.race_entries: N`.
|
||||
|
||||
## Client surfaces
|
||||
|
||||
- `/match.html` — dedicated race queue / race view with economy selector and race HUD.
|
||||
- `/tavern.html` — **Queue for Race** panel and **Active Races** list with Watch buttons. Tavern
|
||||
spectators receive `tavern_match_tick` / `tavern_match_end` events and render races through the
|
||||
existing render kit.
|
||||
|
||||
## Socket events
|
||||
|
||||
Server emits:
|
||||
- `game_mode_info` — now includes `modes.match { enabled, economies, maxPlayers }`.
|
||||
- `match_queue_joined` / `match_queue_left` — queue responses.
|
||||
- `match_joined` / `match_start` / `match_tick` / `match_end` — race lifecycle for players.
|
||||
- `tavern_match_list` / `tavern_match_tick` / `tavern_match_end` — public race feed for tavern
|
||||
spectators.
|
||||
|
||||
Client sends:
|
||||
- `match_queue` `{ economy, action: 'join' | 'leave' }`
|
||||
- `match_move` `{ dx, dy }`
|
||||
- `match_leave` — forfeit an active race.
|
||||
- `tavern_match_list` — request active races.
|
||||
|
||||
## Leaderboards
|
||||
|
||||
- **Free races** create synthetic `games` rows with `game_mode = 'FREE'` for the Pleb board.
|
||||
- **Credits/prestige races** populate the `prestige_leaderboard` view and are served by
|
||||
`GET /api/leaderboard?board=prestige`.
|
||||
- **Crypto races** create synthetic `games` rows with `game_mode = 'PAID_CREDITS'` for the Hall of
|
||||
Champions board.
|
||||
|
||||
## Tests
|
||||
|
||||
Run the match-mode test suites in isolation:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
npx jest ../test/match
|
||||
```
|
||||
|
||||
Current result: **40 passing** across 7 suites:
|
||||
|
||||
- `matchRoom.test.js`
|
||||
- `matchQueue.test.js`
|
||||
- `matchScheduler.test.js`
|
||||
- `matchLeaderboard.test.js`
|
||||
- `matchPayoutService.test.js`
|
||||
- `tavernMatchBridge.test.js`
|
||||
- `matchReconnect.test.js`
|
||||
|
||||
## Files added / modified
|
||||
|
||||
New:
|
||||
- `src/migrations/022_match_mode.sql`
|
||||
- `src/multiplayer/MatchRoom.js`, `MatchEngine.js`, `MatchState.js`
|
||||
- `src/network/matchQueue.js`, `matchScheduler.js`, `matchManager.js`, `matchPayoutService.js`,
|
||||
`matchLeaderboard.js`, `tavernMatchBridge.js`
|
||||
- `html/match.html`, `html/js/matchClient.js`
|
||||
- `docs/MATCH_MODE.md`, `src/.env.match.example`
|
||||
- 7 test files
|
||||
|
||||
Modified:
|
||||
- `src/network/socketHandlers.js` — initializes match services and wires socket events.
|
||||
- `src/network/identityService.js` — adds `userForId()`.
|
||||
- `src/game/gameModeManager.js` — advertises `modes.match`; grants race-entry tickets on product
|
||||
payment.
|
||||
- `src/payments/productGrants.js` — recognizes `grants.race_entries`.
|
||||
- `src/index.js` — `/api/leaderboard?board=prestige`.
|
||||
- `html/tavern.html` — race queue / spectator UI.
|
||||
- `src/.env.example`, `docs/TAVERN_AND_MULTIPLAYER.md`, `docs/TODO.md`, `README.md`.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- Match mode is **opt-in** and inert unless `MATCH_ENABLED=true`.
|
||||
- Crypto race payouts reuse the existing `payouts` table, batch processor, and retry service.
|
||||
- The unique partial index `idx_payouts_one_per_match` prevents double payouts.
|
||||
- Race-entry tickets are held in escrow on queue join and consumed only when a match starts.
|
||||
- Players who disconnect during a race have a 30-second grace period to reconnect.
|
||||
@@ -1,9 +1,10 @@
|
||||
# Tavern & Multiplayer — Design & Build Plan
|
||||
|
||||
**Status:** In development. Shared engine scaffold landed under `src/multiplayer/`; unit tests pass.
|
||||
Match mode (multiplayer races) now has its own dedicated design doc: see `docs/MATCH_MODE.md`.
|
||||
|
||||
Two new, optional game modes. Both are disabled by default and gated by config, so existing
|
||||
single-player play is unaffected.
|
||||
**Status:** Tavern is complete; multiplayer race mode is implemented (see `docs/MATCH_MODE.md`).
|
||||
PvP combat remains a future extension. Both modes are disabled by default and gated by config, so
|
||||
existing single-player play is unaffected.
|
||||
|
||||
## Shared engine: one Room, two modes
|
||||
|
||||
@@ -23,9 +24,9 @@ transport-agnostic makes it unit-testable in isolation and reusable as a standal
|
||||
choose an avatar, move around a shared map, and chat. A spectator camera lets them watch live
|
||||
single-player or multiplayer games from inside the room.
|
||||
|
||||
**Multiplayer** — a shared match Room. Players race to escape a dungeon before the next block,
|
||||
contend with monsters and environmental hazards, and (later) fight each other. Match rooms enable
|
||||
occupant collision; the Tavern does not.
|
||||
**Multiplayer Race** — a shared match Room. Players race to escape a dungeon before the next block
|
||||
and contend with a shared monster. Match rooms enable occupant collision; the Tavern does not.
|
||||
PvP combat is planned for a later milestone.
|
||||
|
||||
## Operator Policy
|
||||
|
||||
|
||||
@@ -1,3 +1,19 @@
|
||||
## Match Mode (Complete)
|
||||
|
||||
All milestones implemented. See `docs/MATCH_MODE.md` for architecture and configuration.
|
||||
|
||||
- [x] Design review: economy models, block-cadence lifecycle, ticket system, premium integration
|
||||
- [x] Database schema: `matches`, `match_entrants`, `match_events`, `match_queue_entries`, `race_entry_transactions`, `users.race_entries`, `payouts.match_id`
|
||||
- [x] Engine scaffold: `MatchRoom`, `MatchEngine`, `MatchState` with unit tests
|
||||
- [x] Per-economy queues and block-cadence `MatchScheduler`
|
||||
- [x] Socket.IO `MatchManager` and free races
|
||||
- [x] Credits/prestige races and separate Prestige leaderboard
|
||||
- [x] Crypto race-entry tickets and winner-take-pot payout
|
||||
- [x] Client (`match.html`, `matchClient.js`) and Tavern spectator integration
|
||||
- [x] Reconnect/AFK tests
|
||||
- [x] Final doc pass and operator deployment notes
|
||||
- [x] Unit tests: 40 passing across 7 suites
|
||||
|
||||
# Wownerogue Development Todo
|
||||
|
||||
## High Priority
|
||||
|
||||
Reference in New Issue
Block a user