mirror of
https://github.com/Such-Software/wownerogue.git
synced 2026-10-26 17:48:32 +01:00
chore: prepare the repository for public release
Removes operator-private material from the shipped tree, corrects documentation
that had drifted from the code, deletes dead code, and applies a consistent
prose style.
Private material moved out of the public tree (docs/private/, git-ignored):
- the wallet keyfile recovery runbook and its helper scripts, which together
published the mainnet wallet file path, the service account, backup snapshot
filenames, and the wallet RPC listener state. The helper duplicated the same
constants, so relocating the document alone would not have removed them.
- the stagenet parallel-candidate runbook and its fixtures.
- the dated production-state record, the internal remediation plan, and the
TODO list. None are reference documentation for an outside reader.
Redacted or genericised:
- operator home-directory paths in shipped scripts and units. The asset build
script now takes KENNEY_ASSETS_DIR instead of a hard-coded path.
- private LAN addresses in tests, env examples and deploy docs, replaced with
RFC 5737 documentation addresses.
- the default alert sender and the notification hub example.
- the Monero wallet unit now matches the Wownero one: a dedicated service
account and a config file, instead of the operator's login account with the
wallet password in argv where any local user can read it from /proc.
Ignore rules now cover every .env variant rather than the bare name, plus local
agent state. The deployment docs instruct operators to create .env.mainnet and
.env.stagenet, which the exact-name rule would have allowed to be committed.
Documentation corrected against the code: per-chain dungeon level counts, the
premium unlock rule, Tavern chat scope, backup retention, the chain-profile
fallback, the environment template location, and several sections describing
shipped work as pending.
Dead code removed: configPersistence, rpccalls, a duplicate vendored ROT.js
bundle, the unused tile-picker overlay, the unreachable legacy game-size path
and its vestigial parameter, spectator stubs that shadowed the real
SpectatorManager, and commented-out debugging. nip98 now imports the shared
nostr loader rather than duplicating it.
Prose style: em-dashes removed throughout in favour of colons, semicolons and
commas; comments restated as present-tense descriptions of the code rather than
narration of past changes.
127 suites / 1015 tests pass.
This commit is contained in:
+176
-140
@@ -1,9 +1,10 @@
|
||||
# Match Mode — Race/PvP Multiplayer
|
||||
# Match Mode: Real-Time Multiplayer Races
|
||||
|
||||
> Status: **Implemented** — all five milestones complete. Single-player and Tavern modes are untouched.
|
||||
Match mode adds real-time multiplayer races. It reuses the shared-world engine (`Room` / `Occupant`),
|
||||
the render kit, the payment and payout systems, and provably-fair seeding. Single-player and Tavern
|
||||
modes are unaffected by it.
|
||||
|
||||
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.
|
||||
Match mode is opt-in. Nothing in this document is active unless `MATCH_ENABLED=true`.
|
||||
|
||||
## Supported economies
|
||||
|
||||
@@ -11,37 +12,43 @@ engine (`Room`/`Occupant`), the render kit, the payment/payout systems, and prov
|
||||
|---|---|---|---|
|
||||
| `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 |
|
||||
| `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.
|
||||
`credits_prestige` spends the same `users.credits` balance used everywhere else; there is no separate
|
||||
"fake credit" system. On an instance running `CREDITS_PAYOUTS_ENABLED=false` those credits are
|
||||
unredeemable, so the economy is prestige-only.
|
||||
|
||||
## Lifecycle: a race every block
|
||||
Crypto races spend non-refundable race-entry tickets (`users.race_entries`, backed by rows in
|
||||
`race_entry_lots`), so leaving a queue before a match starts never requires an on-chain refund.
|
||||
|
||||
The existing solo `queueHandler` already reacts to new crypto blocks. `MatchScheduler` subscribes to
|
||||
the same block-count event. Free and value-bearing queues intentionally have different starts:
|
||||
## Lifecycle
|
||||
|
||||
- A free queue with ≥2 players drains into a `MatchRoom` immediately on the current block event.
|
||||
- A paid queue with ≥2 players at canonical header H atomically creates a `pending` match and links
|
||||
the exact FIFO queue rows and entrants. That transaction records a deterministic entrant-freeze
|
||||
commitment and fixes canonical header H+the configured entropy delay as its target; it does not
|
||||
read a block hash or create a playable seed. A fresh post-commit count is persisted while the
|
||||
`MatchScheduler` subscribes to the same block-count event as the solo `queueHandler`. Free and
|
||||
value-bearing queues start differently on purpose.
|
||||
|
||||
- A free queue with at least the ruleset minimum of players drains into a `MatchRoom` immediately on
|
||||
the current block event.
|
||||
- A paid queue at canonical header H atomically creates a `pending` match and links the exact FIFO
|
||||
queue rows and entrants. That transaction records a deterministic entrant-freeze commitment and
|
||||
fixes canonical header `H + MATCH_PAID_ENTROPY_DELAY_BLOCKS` as its target; it does not read a
|
||||
block hash or create a playable seed. A fresh post-commit daemon count is persisted while the
|
||||
target is still in the future.
|
||||
- Only a later, sufficiently confirmed block-count event can resolve that pending match. The
|
||||
scheduler twice requests the canonical hash for the exact target, verifies the frozen IDs again,
|
||||
derives the seed, and changes the match from `pending` to `starting` before collection/countdown.
|
||||
It immediately checks the hash again. Another height and server randomness are not substitutes.
|
||||
- If a queue has 1 player, they carry over and may leave until a freeze claims their row.
|
||||
scheduler requests the canonical hash for the exact target twice, verifies the frozen IDs again,
|
||||
derives the seed, and moves the match from `pending` to `starting` before ticket collection and
|
||||
countdown. It re-reads the hash immediately after activation. Another height and server randomness
|
||||
are not substitutes.
|
||||
- If a queue is below the minimum player count, its members carry over and may leave until a freeze
|
||||
claims their row.
|
||||
- If a queue is empty, nothing happens.
|
||||
|
||||
A match ends when:
|
||||
1. Its selected ruleset resolves (for example, first exit, last alive, or all players resolved).
|
||||
2. If `ruleset.timing.blockDeadline` is enabled, the first advancing canonical header after both
|
||||
the match's start header and `MATCH_MIN_DURATION_MS` of active play is observed. A duplicate
|
||||
block poll, the start header itself, or a block arriving before the floor cannot end the match;
|
||||
after an early block, the match waits for the next advancing header.
|
||||
|
||||
1. Its selected ruleset resolves (for example first exit, last alive, or all players resolved).
|
||||
2. `ruleset.timing.blockDeadline` is enabled and the first advancing canonical header is observed
|
||||
after both the match's start header and `MATCH_MIN_DURATION_MS` of active play. A duplicate block
|
||||
poll, the start header itself, or a block arriving before the floor cannot end the match; after an
|
||||
early block the match waits for the next advancing header.
|
||||
3. The `MATCH_HARD_CEILING_MS` absolute ceiling expires.
|
||||
|
||||
Final placement and `winnerId` come from the selected ruleset's deterministic rank strategy. The
|
||||
@@ -52,82 +59,111 @@ rankings.
|
||||
|
||||
### 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
|
||||
- `MatchRoom.js` extends `Room`. Owns the shared deterministic dungeon, the shared monster when the
|
||||
ruleset includes one, player life/death/finish state, treasure, and the block/timer deadline. Acts
|
||||
as the synchronous tick resolver.
|
||||
- `MatchEngine.js` is the server tick driver. It wraps a `MatchRoom` with an interval 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`.
|
||||
- `MatchState.js` holds serialization helpers that produce broadcast and persistence shapes,
|
||||
including the renderer-agnostic `gameState` consumed by `html/js/render/sceneModel.js`.
|
||||
|
||||
### Transport / lifecycle layer (`src/network/`)
|
||||
### Transport and 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.
|
||||
- `matchQueue.js` persists per-economy queues (`free`, `credits_prestige`, `crypto_race`), handles
|
||||
join and leave, and performs atomic credit and ticket-lot escrow.
|
||||
- `matchScheduler.js` is the block-cadence scheduler that freezes, activates, and drains queues into
|
||||
matches.
|
||||
- `matchFairness.js` builds and verifies the paid entrant-freeze commitment and derives match seeds.
|
||||
- `matchEconomyPolicy.js` is the single fail-closed admission contract for crypto matches, shared by
|
||||
the queue, scheduler, mode advertisement, and environment validator.
|
||||
- `matchManager.js` owns Socket.IO rooms, match lifecycle, broadcasts, reconnect grace, and
|
||||
persistence.
|
||||
- `matchPayoutService.js` handles crypto-race pot accounting, house-fee split, and the winner payout
|
||||
record.
|
||||
- `matchLeaderboard.js` posts match scores to the correct leaderboard.
|
||||
- `tavernMatchBridge.js` forwards public match state to the tavern room for spectators.
|
||||
|
||||
### Rulesets (`src/game/rulesets/`)
|
||||
|
||||
Built-in match rulesets are `race`, `last-alive`, `score-attack`, and `coop-escape`; `solo-classic`
|
||||
is solo-only. `resolveMatchRuleset()` fails closed to `race` for an unknown or solo-only id. See
|
||||
[RULESETS.md](RULESETS.md).
|
||||
|
||||
### Persistence
|
||||
|
||||
See migration `src/migrations/022_match_mode.sql`:
|
||||
Schema lives in `src/migrations/`:
|
||||
|
||||
- `matches` — one race, including the durable `pending` entrant-freeze envelope and later
|
||||
verifiable seed commitment, 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.
|
||||
- `022_match_mode.sql` creates `matches`, `match_entrants`, `match_events`, `match_queue_entries`,
|
||||
and `race_entry_transactions`, adds `users.race_entries` and `payouts.match_id`.
|
||||
- `029_match_ruleset_id.sql` records the ruleset that produced each match.
|
||||
- `033_match_payout_liability.sql` adds `race_entry_lots` (per-payment funded ticket lots), links
|
||||
queue entries and entrants to their exact lot, and makes the accepted match liability immutable.
|
||||
- `040_paid_match_entropy_precommit.sql` adds `entropy_precommit_tip_height` and
|
||||
`entropy_precommit_verified_at`, with a check constraint requiring the witnessed tip to be below
|
||||
the target height for paid economies.
|
||||
|
||||
`032_solo_liability_invariants.sql` defines the unconditional unique index
|
||||
`idx_payouts_one_per_match`.
|
||||
|
||||
Tables:
|
||||
|
||||
- `matches` is one race, including the `pending` entrant-freeze envelope, the verifiable seed
|
||||
commitment, economy, pot and fee, and winner.
|
||||
- `match_entrants` is per-player state, placement, score, and payment link.
|
||||
- `match_events` is the replay, spectator, and audit feed.
|
||||
- `match_queue_entries` is the persisted queue, for restart safety.
|
||||
- `race_entry_transactions` is the race-entry ticket ledger.
|
||||
|
||||
### Match seed verification
|
||||
|
||||
Paid matches use `future-block-freeze-v2` / `future-chain-block-v2`. At canonical header H the
|
||||
server commits the exact economy, ruleset, sorted durable FIFO queue-entry IDs, configured delay,
|
||||
and target `H + MATCH_PAID_ENTROPY_DELAY_BLOCKS` in a `pending` row before requesting any hash.
|
||||
After that transaction commits, a fresh strict daemon count must prove that the target does not yet
|
||||
exist; the witnessed tip and verification timestamp are durable and immutable. A delayed commit
|
||||
that reaches the target is cancelled and every exact escrow anchor is refunded without reading the
|
||||
target hash. Legacy v1 or otherwise unverified pending freezes are also refunded at startup.
|
||||
Paid matches use the `future-block-freeze-v2` freeze and `future-chain-block-v2` seed versions. At
|
||||
canonical header H the server commits the exact economy, ruleset, sorted durable FIFO queue-entry
|
||||
IDs, configured delay, and target `H + MATCH_PAID_ENTROPY_DELAY_BLOCKS` into a `pending` row before
|
||||
requesting any hash. After that transaction commits, a fresh strict daemon count must prove the
|
||||
target does not yet exist; the witnessed tip and verification timestamp are durable and immutable. A
|
||||
delayed commit that reaches the target is cancelled and every exact escrow anchor is refunded without
|
||||
reading the target hash. Legacy v1 and otherwise unverified pending freezes are refunded at startup.
|
||||
|
||||
Activation waits until the target itself has the configured confirmation depth (the target counts
|
||||
as confirmation one). The scheduler reads count/header/count/header and requires both exact-height
|
||||
headers to have the same canonical hash. It re-reads that target immediately after the activation
|
||||
transaction and before ticket collection, gameplay, or client notification; an unavailable or
|
||||
changed hash aborts and refunds the match. The playable seed is SHA-256-derived only from the v2
|
||||
freeze commitment and exact target hash. The persisted/public proof records the delay, required
|
||||
Activation waits until the target itself has the configured confirmation depth, where the target
|
||||
counts as confirmation one. The scheduler reads count, header, count, header, and requires both
|
||||
exact-height headers to carry the same canonical hash. It re-reads that target immediately after the
|
||||
activation transaction and before ticket collection, gameplay, or client notification; an unavailable
|
||||
or changed hash aborts and refunds the match. The playable seed is SHA-256 derived only from the v2
|
||||
freeze commitment and the exact target hash. The persisted public proof records the delay, required
|
||||
confirmations, minimum activation tip, and post-commit witness. Confirmation settings are safety
|
||||
metadata rather than extra seed material.
|
||||
metadata, not extra seed material.
|
||||
|
||||
Monero-family `getblockcount` is a count, while `get_block(height)` uses zero-based header heights.
|
||||
The scheduler normalizes the event exactly once (`observedHeaderHeight = blockCount - 1`). Every
|
||||
persisted/disclosed `blockHeight` is the actual header height passed unchanged to `get_block`, so the
|
||||
published height resolves to the published hash without an off-by-one translation.
|
||||
Monero-family `getblockcount` returns a count while `get_block(height)` uses zero-based header
|
||||
heights. The scheduler normalizes the event exactly once (`observedHeaderHeight = blockCount - 1`).
|
||||
Every persisted or disclosed `blockHeight` is the actual header height passed unchanged to
|
||||
`get_block`, so a published height resolves to the published hash with no off-by-one translation.
|
||||
|
||||
This proves the documented seed derivation and deterministic dungeon; it does not prove honest
|
||||
input handling, block-source independence, payout delivery, or resistance to a malicious chain
|
||||
producer/operator. Free development matches may use a server-random commitment and are labelled
|
||||
`server commitment only`. Do not describe match mode as equivalent to the solo two-party fairness
|
||||
Free matches derive their seed from the current block hash when one is available. When it is not,
|
||||
they fall back to a server-random seed recorded as derivation version `server-random-v1`. Paid
|
||||
economies have no such fallback: a paid match without a `future-chain-block-v2` proof is retained
|
||||
rather than started.
|
||||
|
||||
This construction proves the documented seed derivation and the deterministic dungeon. It does not
|
||||
prove honest input handling, block-source independence, payout delivery, or resistance to a malicious
|
||||
chain producer or operator. Do not describe match mode as equivalent to the solo two-party fairness
|
||||
protocol without an independent review.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to `src/.env` (documented in `src/.env.example`):
|
||||
Set these in `src/.env`. The full annotated template is `src/.env.example`; a match-only excerpt is
|
||||
`src/.env.match.example`.
|
||||
|
||||
```bash
|
||||
# Enable match mode (default false)
|
||||
MATCH_ENABLED=false
|
||||
|
||||
# Max players per match (also bounded by the selected ruleset; built-ins max at 8)
|
||||
# Max players per match, also bounded by the selected ruleset (built-ins max at 8)
|
||||
MATCH_MAX_PLAYERS=4
|
||||
|
||||
# Server tick interval in ms
|
||||
MATCH_TICK_MS=250
|
||||
|
||||
# Server-selected gameplay ruleset; clients cannot override it
|
||||
# Server-selected gameplay ruleset; clients cannot override it.
|
||||
# race | last-alive | score-attack | coop-escape
|
||||
MATCH_RULESET_ID=race
|
||||
|
||||
@@ -148,7 +184,7 @@ MATCH_CREDITS_COST=1
|
||||
# Crypto race house fee percent (required; 0 <= fee < 100)
|
||||
MATCH_HOUSE_FEE_PERCENT=5
|
||||
|
||||
# Required per-player funded ticket value in atomic units.
|
||||
# Required per-player funded ticket value in atomic units
|
||||
MATCH_ENTRY_FEE_ATOMIC=5000000000
|
||||
|
||||
# Crypto admission requires both explicit match gates and an outer atomic-unit payout cap
|
||||
@@ -157,103 +193,103 @@ MATCH_PAYOUTS_ENABLED=false
|
||||
MATCH_PAYOUT_MAX=50000000000
|
||||
```
|
||||
|
||||
Crypto races also require:
|
||||
- A paid instance (`GAME_MODE=PAID_SINGLE` / `PAID_CREDITS` or equivalent `PAYMENT_MODES`).
|
||||
- At least one race-entry product with `grants.race_entries: N` and
|
||||
`grants.race_entry_value_atomic` exactly equal to `MATCH_ENTRY_FEE_ATOMIC`. The product price
|
||||
must cover `N * MATCH_ENTRY_FEE_ATOMIC`; confirmation records a durable lot per payment.
|
||||
- A competitive single-winner ruleset. `coop-escape` is rejected until split-payout semantics are
|
||||
implemented.
|
||||
Crypto races additionally require:
|
||||
|
||||
- A paid instance (`GAME_MODE=PAID_SINGLE` or `PAID_CREDITS`, or the equivalent `PAYMENT_MODES`).
|
||||
- At least one race-entry product with `grants.race_entries: N` and `grants.race_entry_value_atomic`
|
||||
exactly equal to `MATCH_ENTRY_FEE_ATOMIC`. The product price must cover `N * MATCH_ENTRY_FEE_ATOMIC`;
|
||||
confirmation records a durable lot per payment.
|
||||
- A competitive single-winner ruleset. `coop-escape` is rejected because it has no
|
||||
winner-takes-pot split contract.
|
||||
|
||||
## 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.
|
||||
- `/match.html` with `html/js/matchClient.js` is the dedicated race page: entry-mode selector,
|
||||
ruleset catalog, race HUD, render-style picker, and touch d-pad.
|
||||
- `/tavern.html` adds a Multiplayer Arena panel with the entry-mode selector and an active-match list
|
||||
with Watch buttons. Tavern spectators receive `tavern_match_tick` and `tavern_match_end` and render
|
||||
races through the render kit. A tavern viewer who is a player in the match keeps movement controls
|
||||
instead of a spectator camera.
|
||||
|
||||
## Socket events
|
||||
|
||||
Server emits:
|
||||
- `game_mode_info` — includes `modes.match { enabled, economies, maxPlayers, activeRuleset, rulesets }`.
|
||||
- `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
|
||||
|
||||
- `game_mode_info` includes `modes.match { enabled, economies, maxPlayers, activeRuleset, rulesets }`.
|
||||
- `match_queue_joined` and `match_queue_left` are queue responses.
|
||||
- `match_joined`, `match_start`, `match_tick`, `match_end` are the race lifecycle for players.
|
||||
- `match_rejoined` restores a reconnecting player; `match_settlement_pending` reports a result whose
|
||||
payout is still settling.
|
||||
- `player_forfeit` and `player_death` are in-match participant updates.
|
||||
- `match_error` is the benign catch-all failure response for any match request.
|
||||
- `tavern_match_list`, `tavern_match_tick`, `tavern_match_end` are the 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.
|
||||
- `match_reconnect` to rejoin an active or just-finished match after a reconnect.
|
||||
- `match_leave` to forfeit an active race.
|
||||
- `tavern_match_list` to request active races.
|
||||
|
||||
Identity for every match request is resolved from the connection, never from the payload.
|
||||
|
||||
## Leaderboards
|
||||
|
||||
- **Free competitive matches** create synthetic `games` rows with `game_mode = 'FREE'` for the
|
||||
Pleb board.
|
||||
- **Credits/prestige competitive matches** are served from authoritative `matches` and
|
||||
- Free competitive matches create synthetic `games` rows with `game_mode = 'FREE'` for the Pleb
|
||||
board.
|
||||
- Credits/prestige competitive matches are served from the authoritative `matches` and
|
||||
`match_entrants` rows by `GET /api/leaderboard?board=prestige`.
|
||||
- **Crypto competitive matches** create synthetic `games` rows with `game_mode = 'PAID_CREDITS'`
|
||||
for the generic self-hosted Hall of Champions board. Such Software's two operated profiles keep
|
||||
this economy disabled and exclude all match-generated rows from Champions.
|
||||
- Crypto competitive matches create synthetic `games` rows with `game_mode = 'PAID_CREDITS'` for the
|
||||
generic self-hosted Hall of Champions board. The operated product profiles
|
||||
(`such-play-wow-prestige`, `such-monerogue-stagenet`) keep this economy disabled and exclude all
|
||||
match-generated rows from Champions.
|
||||
|
||||
The public leaderboard endpoint requires `board=pleb|champions|prestige`; omitting `board` safely
|
||||
defaults to `pleb`. The removed mixed `board=all` behavior and unknown or blank values return 400
|
||||
instead of silently combining or translating economies.
|
||||
The public leaderboard endpoint accepts `board=pleb|champions|prestige`. Omitting `board` defaults to
|
||||
`pleb`; unknown or blank values return 400 rather than silently combining or translating economies.
|
||||
|
||||
An individual win requires the durable match winner and placement #1 to agree; reaching the exit
|
||||
alone is never treated as a win in last-alive or score-attack. `coop-escape` is collective, records
|
||||
no individual `winnerId`, and keeps its results in `matches` / `match_entrants`; it is intentionally
|
||||
excluded from individual boards until a dedicated team leaderboard exists.
|
||||
no individual `winnerId`, and keeps its results in `matches` and `match_entrants`. It is excluded
|
||||
from individual boards.
|
||||
|
||||
## Tests
|
||||
|
||||
Run the match-mode test suites in isolation:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
npx jest ../test/match
|
||||
npx jest test/match
|
||||
```
|
||||
|
||||
The suite includes block-deadline, authoritative leaderboard-result, co-op exclusion, durable
|
||||
finish, and finished-room reconnect regressions in addition to the core room/queue/payout tests.
|
||||
|
||||
## 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`.
|
||||
The suites cover the room, queue, scheduler, payout service, and leaderboard, plus block-deadline,
|
||||
authoritative leaderboard-result, co-op exclusion, durable finish, crash recovery, client
|
||||
settlement, and finished-room reconnect regressions.
|
||||
|
||||
## Operational notes
|
||||
|
||||
- Match mode is **opt-in** and inert unless `MATCH_ENABLED=true`.
|
||||
- `MATCH_RULESET_ID` is trusted server configuration; unknown/solo-only values fall back to `race`.
|
||||
- `MATCH_RULESET_ID` is trusted server configuration. Unknown or solo-only values fall back to
|
||||
`race`.
|
||||
- Crypto race payouts reuse the existing `payouts` table, batch processor, and retry service.
|
||||
- `MATCH_PAYOUTS_ENABLED=false` stops new admission and refunds queued ticket escrow. Matches whose
|
||||
pot/liability was already accepted still create their durable payout; the global payout master
|
||||
switch may pause dispatch without erasing it.
|
||||
- `MATCH_PAYOUTS_ENABLED=false` blocks new crypto admission. Queue entries persisted for a
|
||||
now-unavailable economy are cancelled and refunded transactionally during queue initialization.
|
||||
Matches whose pot and liability were already accepted still create their durable payout from the
|
||||
immutable admission snapshot; the global payout master switch may pause dispatch without erasing
|
||||
it.
|
||||
- The unconditional unique index `idx_payouts_one_per_match` prevents a replacement payout in every
|
||||
status, including `failed` and `needs_review`.
|
||||
- Only confirmed-payment ticket lots with the current exact entry value can enter a crypto race;
|
||||
legacy/admin-granted aggregate tickets cannot create a payout liability. The exact lot is held
|
||||
- Only confirmed-payment ticket lots with the current exact entry value can enter a crypto race, so
|
||||
legacy or admin-granted aggregate tickets cannot create a payout liability. The exact lot is held
|
||||
on queue join and consumed in the same transaction that accepts the immutable match liability.
|
||||
- Players who disconnect during a race have a 30-second grace period to reconnect.
|
||||
- Paid freeze/start block events are serialized. A crash before activation leaves a resumable
|
||||
`pending` freeze; a crash after activation is handled by abandoned-match recovery. Orderly
|
||||
shutdown stops the scheduler, waits for its block task, and transactionally cancels/refunds every
|
||||
- Players who disconnect during a race have a 30-second grace period to reconnect before an AFK kill.
|
||||
Match mappings are retained for a further 30 seconds after a finish so late reconnects still
|
||||
resolve the result.
|
||||
- Paid freeze and start block events are serialized. A crash before activation leaves a resumable
|
||||
`pending` freeze; a crash after activation is handled by abandoned-match recovery. Orderly shutdown
|
||||
stops the scheduler, waits for its block task, and transactionally cancels and refunds every
|
||||
still-pending freeze before shutting down match transport.
|
||||
|
||||
## See also
|
||||
|
||||
- [TAVERN_AND_MULTIPLAYER.md](TAVERN_AND_MULTIPLAYER.md)
|
||||
- [RULESETS.md](RULESETS.md)
|
||||
- [PRODUCTION_DISCLOSURES.md](PRODUCTION_DISCLOSURES.md)
|
||||
|
||||
Reference in New Issue
Block a user