Commit Graph
68 Commits
Author SHA1 Message Date
jwinterm dacbf356ed fix: close pre-launch review findings and light the premium renderers
Adversarial multi-agent review of the launch candidate produced 22 confirmed
findings (12 more were refuted). This closes the critical, high, and the
mediums/lows that are cheap and real, then does the iso/3D/Tavern art pass.

CRITICAL — unauthenticated remote kill switch
  Socket.IO does not wrap listener invocation, and this process treats
  uncaughtException/unhandledRejection as fatal, so ONE unguarded throw in ANY
  listener exits the server. `socket.emit('debug_ping')` with no argument did
  exactly that, and systemd restarts straight back into it. Every registration
  in handleConnection now goes through a safe-dispatch wrapper that contains
  sync throws and rejected promises to the offending socket; handleDebugPing
  also stops trusting its payload.

HIGH
  - NIP-98 login now binds the signed event to this deployment's host. The path
    check alone let an event signed for any other origin authenticate here, and
    linking ADOPTS the account owning that pubkey — full account takeover.
  - Fog of war is enforced server-side. getState() shipped absolute
    entrance/exit/treasure coordinates and the monster's exact position in every
    frame, with concealment implemented only in the browser. Discovery is sticky;
    the monster is reported only while in view.
  - NODE_ENV must now be explicit and recognized. Unset, every production gate
    read false while DebugManager reported production and silenced logging.
  - Tavern chat applies the lobby's ban list, rate limiter and user_id
    attribution before entering global chat.
  - userRegistry is deleted by socket.id (it was keyed by socket.id and deleted
    by socket.client.id, so nothing was ever freed).

MEDIUM / LOW
  - Payouts: a legitimately split transfer_split kept its transaction hashes
    instead of quarantining already-broadcast funds with tx_hash NULL, and
    provably pre-broadcast failures return to 'pending' rather than needs_review.
  - RateLimiter key parsing fixed — cleanup() freed nothing and expired windows
    never reset, because ids contain colons.
  - Enter no longer submits a paid entry from behind a modal.
  - Lobby chat no longer double-escapes server-escaped text.
  - Rate limits for identity:get / address:prompt / early_entry.
  - Spectators released when a game ends by disconnect, not just settlement.
  - movementManager / addressManager per-socket maps cleaned on disconnect.

Renderers
  Iso: opaque shading instead of translucent fog, additive torch/lantern/beacon
  light pools, crevice AO, embers, cold memory wash, and it stops reallocating
  the whole canvas every animation frame.
  3D: instanced level geometry (3 draw calls), procedural stone/flagstone
  materials, torch lights + shadows, real meshes for stairs/chest/monster
  (they were humanoid capsules), dust motes, wall cutaway, and lighting
  rebalanced now that fog is measured from the camera.

Tavern
  Block height and hash fragments drift around the stage edge and ripple when a
  block lands, fed by a cosmetic chain tip added to the blockheight broadcast.
  Speech bubbles work again (they keyed off a socketId the server stopped
  sending).

126 suites / 1005 tests pass.
2026-07-26 07:29:47 -04:00
jwinterm ca8e189b20 fix: enforce operated paid mode contracts 2026-07-23 20:31:20 -04:00
jwinterm 6a3d5f8191 feat: expose immutable release identity 2026-07-23 13:27:17 -04:00
jwinterm cbaca7d45b fix: enforce operated product boundaries 2026-07-23 10:44:32 -04:00
jwinterm 80d7f35e1f feat: preserve solo runs across production restarts 2026-07-22 15:05:49 -04:00
jwinterm 89398335c7 feat: productionize paid modes and PvP capture 2026-07-21 20:41:27 -04:00
jwinterm dd7d70aec8 fix: fog-of-war entities, multi-level descend feedback, camera centering, fewer levels
- FOG: sceneFromGameState only emits exit/entrance/treasure/items once their cell is EXPLORED,
  and the monster only while IN VIEW — no more exit stairs leaking through the fog.
- DESCEND FEEDBACK: reaching a non-final exit descends (multi-level) and used to silently dump the
  player at a new entrance (read as a bug). Server now sends depth/maxDepth on game_event; the
  client shows "Stairs down! Level N of M — keep going". Also lowered level counts (WOW 4->2,
  BTC 8->3, XMR 2->1) — clearing 4-8 levels in one block was unwinnable; kept modest until proper
  multi-level calibration + UX.
- CAMERA: the host filled the very-tall #game-display so centering put the player low; it's now a
  bounded viewport (min(100%,78vh)) centered in #game-display, so the player stays on-screen-centered.
Suite 62/435 green.
2026-07-12 11:50:40 -04:00
jwinterm 31b4c3ca5d feat(entry): START GAME shows the wait-vs-drop-in choice for free games too
Previously the timing-choice modal (Start Now / Wait for Next Block) only appeared for
paid-credits players; free players got a silent immediate drop-in. Now it shows whenever
the game would start instantly:
- Server getGameModeInfo exposes freePlayEnabled (was server-only) so the client knows
  free play is available even on a paid instance.
- Client (inputHandler) gates the modal on free-play-available OR paid-credits-with-credits;
  the modal is purely a TIMING pick (auto_start vs join_queue) — payment method is unchanged.
- Modal cost text reflects free play.

Both waiting animations are untouched: they render on the ROT display via WaitingScreenAnimator
using the base GameTiles, independent of the render-kit packs (which are disabled for SP anyway),
so they work regardless of visualization style. Suite 62/435 green.
2026-07-12 10:44:31 -04:00
jwinterm c5147de625 feat(game): multi-level dungeons — descend N levels, timer-driven edge
The fair, on-theme edge lever for slow chains (from the calibration work): a run
now descends `maxDepth` levels instead of one map, so cumulative run length makes
the block-timer bite WITHOUT a giant single dungeon or a cheating-fast monster.

Engine (game.js):
- depth/maxDepth; _generateLevel(depth) builds each level (fresh dungeon + monster
  + FOV), _descend() steps down. Reaching a non-final exit descends (event
  "descend", stays active); only the final exit escapes (event "escaped").
- Treasure lives only in the vault (final level) — intermediate levels race to the
  stairs. Single-level (maxDepth=1) is byte-identical to before.
- Provably fair per level: levelSeed(masterSeed, depth) (provablyFair.js) — level 1
  = master seed, deeper levels salted; the whole descent regenerates from the one
  committed seed. Verified: game levels == regenerateFromSeed(levelSeed(seed,d)).

Pacing (difficultyConfig.js): NETWORK_TUNING now sets LEVELS ∝ block time (GRIN 1,
XMR 2, LTC 2, WOW 4, BTC 8) — replacing the size-scale hack; size + monster stay at
the fair preset. DUNGEON_LEVELS operator override; NETWORK_TUNING_DISABLED → 1 level.

Wiring: socketHandlers afterPlayerMove gets a "descend" branch (no game-over, no
monster move on arrival). Client gameState.js clears the accumulated map/explored/
visible tiles on a depth change so the old level does not ghost through the new one.

Sim: explorer bot resets fog-of-war on descent so multi-level sims stay honest
(full re-calibration of the level counts is the pending follow-up).

Tests: multiLevel_ultra (descent, vault-only treasure, per-level provably-fair,
single-level default, DUNGEON_LEVELS) + networkTuning rewritten for the levels model.
Suite 61 suites / 432 green; end-to-end 4-level WOW run descends 3x then escapes.
2026-07-11 22:26:10 -04:00
jwinterm f982f56181 fix(chain): WOW block time is 5 min, not 2 (measured); drop cheating-monster tuning
WOW meanBlockTimeMs 120000 -> 300000. Measured 5.01 min/block over 1000 blocks on
the live daemon; the code comment claiming it was "corrected to 2 min" was backwards.
WOW is a SLOW chain, closer to BTC than XMR for calibration.

Per user feedback ("fast monster feels like cheating") + what the sim then proved:
a supernaturally fast monster doesnt even WORK on slow chains — WOW (5-min) caps
~64% house-win even at a 2.2x monster on a 111x55 map, because a single dungeon
clears in ~1 min << the block so the timer (the fair, on-theme edge) barely bites.
So NETWORK_TUNING drops the monster-speed lever entirely and keeps size∝√blockTime
only (monster stays at its fair preset speed). WOW sizeScale 1.0->1.6 for 5-min blocks.

Consequence: slow chains (WOW/BTC) run honest-but-under-target on the timer until
MULTI-LEVEL DEPTH lands — the real edge lever (cumulative run length across levels
without any single map being an unplayable slog). Deb (WOW/normal) now gets a bigger,
more block-appropriate dungeon (45x22 -> 72x35) with a fair monster.

meanBlockTimeMs has no runtime consumer yet (metadata) so the WOW fix is behavior-safe
today; it corrects the sim + future block-bounded features. Suite 60/428 green.
2026-07-11 20:24:51 -04:00
jwinterm 63d2f5281d feat(balance): per-network difficulty tuning — cryptoType finally shapes size+monster
Solves the two gaps the sim measured (edge undershot targets AND swung ~15pts
across chains from identical dungeons because cryptoType was a dead parameter).

Data-driven design (src/sim/calibrate.js, rewritten): the size-probe showed
dungeon SIZE self-cancels as a house-edge lever — bigger map lengthens the run
(timer edge up) but gives more room to dodge (monster edge down), flat ~60% until
monstrous. So split the concerns:
  • SIZE ∝ √blockTime  → PACING only (consistent run-length fraction of the block;
    GRIN sprint .. BTC epic), applied to every preset. Bot-robust.
  • MONSTER speed       → the house EDGE lever (monotonic), solved per network for
    the casino/70% target only.

difficultyConfig.js: NETWORK_TUNING table + applyNetworkTuning(), folded into
getDifficultyConfig with precedence preset → tuning → env → customOverrides (so
operator DUNGEON_*/MONSTER_* still win; NETWORK_TUNING_DISABLED kills it).

Wired verification (explorer bot, 160 runs, 320ms): WOW/XMR/LTC/BTC/GRIN all land
in a ~66-76% band around target (was 48-65%). BTC 48%->73% via 112x56 + 2.2x
monster — on 10-min blocks the timer barely bites a 30s run, so the monster carries
the edge (caps ~64% maxed: a real property of block-timed play).

CAVEAT baked into the config comment: sim bots do not evade, so monsterSpeed is a
STARTING POINT to retune with live telemetry; the sizeScale half is bot-robust.
Deb is unaffected (WOW=reference scale 1.0; normal preset gets no monster tuning).

test/networkTuning_ultra.test.js: size scaling, casino-only monster, block-time
ordering, env-override-wins, kill-switch. Suite 60 suites / 428 tests green.
2026-07-11 20:02:39 -04:00
jwinterm 5753e919e5 feat(game): Ruleset engine + PvP modes on the match engine (Pillar 4)
Gameplay becomes a data object. A Ruleset captures world/entities/players/
win-condition/timing/economy, and MatchRoom now READS it instead of hardcoding
'first to the exit wins' — so new modes are data, not new subsystems.

- src/game/rulesets/Ruleset.js: defineRuleset normalizer (frozen, clamped, safe
  defaults) + WIN/ECONOMY enums.
- src/game/rulesets/winConditions.js: strategies with onExit/onDeath/rank hooks.
  FIRST_TO_EXIT is a verbatim port of the old race logic (so existing matches are
  byte-identical); LAST_ALIVE (PvP battle-royale), HIGH_SCORE (score attack) and
  ALL_ESCAPE (co-op) are new.
- src/game/rulesets/registry.js: built-ins (solo-classic descriptor, race,
  last-alive, score-attack, coop-escape) + getRuleset/listRulesets +
  rulesetFromMatchOpts (legacy opts -> race, preserved).
- MatchRoom: resolves a ruleset (explicit object / rulesetId / legacy opts),
  delegates win/lose/rank to the strategy, honors entities.monster (optional
  monster) and entities.pvpCombat (stepping onto a living rival strikes them
  down; attacker holds position), and takes timing from the ruleset. deathOrder
  tracked for the last-alive tiebreak.

The live single-player engine is intentionally NOT refactored (represented as the
'solo-classic' descriptor for catalog parity only — too risky to touch the
deployed path). PvP modes are opt-in via rulesetId; the scheduler still builds
'race' so nothing changes until wired.

Behavior-preserving: all 5 existing match suites (42 tests) green unchanged.
Tests: +9 (ruleset normalize/registry + real-MatchRoom last-alive kill->winner,
high-score no-instant-win, race-unchanged). Suite 55 suites / 391 tests green.
2026-07-10 19:20:23 -04:00
jwinterm 74753c17b2 feat(payments): wire provider registry into invoice creation + fit real BTCPay infra
Recon of the operator's LAN BTCPay box (ssh four-btcpay / 10.42.1.33) showed the
real topology: BTCPay Server 2.3.9 handles BTC/LTC only (BTCPAY_CHAINS=ltc,btc),
while XMR and WOW run through separate 'xmrcheckout' (:8080) and 'wowcheckout'
(:8180) apps — and crucially, both expose a BTCPay-Greenfield-COMPATIBLE API
(/api/v1/stores/{id}/invoices, token auth, Settled/New/Expired status). Grin
isn't deployed there yet. So one Greenfield client class serves all three.

- btcpayProvider: id is now constructor-configurable so the same class registers
  as 'btcpay' (BTC/LTC), 'xmrcheckout' (XMR), and 'wowcheckout' (WOW).
- buildProviderRegistry: activates each gateway independently from its *_URL/
  _STORE_ID/_API_KEY trio (BTCPAY_*, XMRCHECKOUT_*, WOWCHECKOUT_*), with
  topology-correct default routing (BTC/LTC->btcpay, XMR/WOW->their checkout or
  native), BTCPAY_CHAINS to widen, PAYMENT_ROUTING to override.
- gameModeManager: takes an optional paymentProviders registry (defaults to a
  native-wrapping registry built from walletService). createPaymentRequest now
  creates the invoice via getProvider(cryptoType).createInvoice(...). The native
  provider delegates to walletService.createPaymentRequest, so the shipped
  single-chain config is byte-for-byte unchanged — same subaddress, same
  monitoring maps. paymentRequest now also carries invoiceId for the monitoring
  wire to come.
- nativeMoneroProvider: surfaces addressIndex so the call site stays provider-agnostic.

Behavior-preserving: no gateway env => native-only, every chain routes native.
Monitoring + payout paths intentionally left on the native path this pass;
gateway monitoring (provider.startWatch) is the flagged next step. Tests: +4
(provider wiring routes to a gateway; default registry delegates to native).
Suite 53 suites / 365 tests green.
2026-07-10 17:17:49 -04:00
jwinterm f9d49f34ee feat(chain): ChainProfile registry for Smirk's 5 chains + centralize decimals
New src/chain/chainProfile.js — one data-driven source for chain params (decimals, mean block time, adapter family, URI scheme) across WOW/XMR/BTC/LTC/GRIN, all PoW/high-variance. inferCurrencyDecimals now delegates to it: WOW=11/XMR=12 unchanged, and BTC=8/LTC=8/GRIN=9 are fixed (were silently defaulting to 12). This is the seam the daemon/wallet/address adapters and the block-time difficulty-scaling plug into. +6 tests, suite 50/350 green.

P2 foundation on branch feat/credits-catalog-foundation.
2026-07-10 16:38:25 -04:00
jwinterm 3f12d8f047 feat(credits): client consumes served catalog; direct entry counts as a credit purchase
① assetPacks.js merges the server-authoritative catalog (from the entitlement snapshot) into RK.PACKS — operator-added packs now appear client-side and existing packs pick up server definitions, retiring the client half of the split-brain. ② A confirmed direct/single_game payment now records as 'buy 1 credit + spend it': balance nets to zero but total_credits_purchased advances (recordDirectEntryPurchase), so direct play unlocks the same tier/threshold cosmetics as buying credits, and the client gets a fresh entitlement snapshot. +2 tests. Suite 49/345 green.

P1 foundation (branch feat/credits-catalog-foundation). Remaining: the buy modal reflects the unified credits model; a later pass can fully collapse single_game into the credit spend path.
2026-07-10 16:28:31 -04:00
jwinterm a9fad69038 fix(match): reconcile no-address winnings on address set; age-guard boot recovery
Follow-up 1: when a user sets a payout address, gameModeManager.setUserPayoutAddress now converts any claimable match winnings recorded with no address (reason 'match_winner_no_address', status 'needs_review', sentinel 'PENDING_NO_ADDRESS') into sendable 'pending' payouts and kicks the batcher. The reason filter is narrow so it never touches the ambiguous-broadcast needs_review rows (which would double-pay).

Follow-up 2: match rows are now marked status='active' with started_at when the countdown completes, and _recoverAbandonedMatches gains a lock-free age guard — single-instance (default) reclaims all in-flight rows immediately, multi-instance (MATCH_SINGLE_INSTANCE=false) only reclaims rows older than the hard ceiling + buffer, so a race running on a sibling instance is never cross-cancelled.

Adds test/followups_ultra.test.js (5 tests). Full suite: 46 suites / 317 tests pass.
2026-07-09 19:18:34 -04:00
jwinterm a62eefb47b fix: resolve security/correctness review + AAA graphics pass
Backend (Tier 0/1/3): single-payout double-pay -> needs_review; admin-retry tx_hash guard; UNIQUE index on games.payment_id (migration 023) + atomic claim; reconnect no longer resets block-timeout; completeGame keys on user_id+dungeon_seed (not socket_id); token-gated /api/user/:socketId/* endpoints; payout min/max enforcement; orphaned-active-game boot recovery; sessionManager cleanup FK fix; XFF rightmost-hop; global uncaughtException/unhandledRejection guard; server binds 127.0.0.1; provably-fair client-seed (HMAC) + RNG [0,1) bound; difficulty preset ranges reach the digger; chat no longer leaks full socket.id; removed dangerous-keyword SQL theater.

Match mode (Tier 2, still MATCH_ENABLED-gated): fixed match_queue/match_move crash paths + scheduler dead-code + leaked ceiling timer; pot collected before start with abort+refund; BigInt/basis-point fee math, capped payout; no-address winner claimable; winnerId==placement[0]; finalize schema + payout-first isolation; reconnect re-key with ownership; refunds on all cancel paths; real countdown; map/timer cleanup. +16 tests.

Graphics: new overlay FX layer (torch light, vignette, embers, sparkle/burst/flash/shake, self-suspending rAF); gameplay juice; atmospheric screens; CSS design-token chrome with framed modals; richer Kenney tile renderer; imported Kenney dungeon/interior sheets + fx particles.

Verification: 45 suites / 312 tests pass; 40 changed JS files parse; 20 server modules load clean; no regressions.
2026-07-09 17:46:58 -04:00
jwinterm 0f68eec26b 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.
2026-07-09 13:26:52 -04:00
jwinterm 454e6ab69a feat: add tavern render kit and character entitlements 2026-07-05 20:57:46 -04:00
jwinterm 63e3006aca launch features: win feed, public stats, share-a-win, live CTA
Four pre-launch additions to make the game feel alive and shareable:

- Live win feed: server broadcasts 'win_feed' on every escape; client shows a
  transient toast ('X escaped with the bag') so the room feels active.
- Public stats: GET /api/stats (10s-cached) returns games + escapes in the last
  24h and total paid out; a status-area strip polls it for social proof.
- Share-a-win: game-over screen gets 'Share to X' + 'Copy link' buttons pointing
  at the /verify page, which now renders OpenGraph/Twitter-card meta (brand OG
  image per instance) so shared links unfurl with a preview.
- Land-on-action: a dismissible 'N games live now — watch' CTA appears on connect
  when games are in progress, opening the existing spectator panel. Non-intrusive:
  hidden while playing/spectating and never hijacks the canvas.
2026-06-01 09:17:48 -04:00
jwinterm db5ed2e8aa payouts: defer when Monero outputs are locked instead of failing
Spent outputs (including change) lock for ~10 blocks on Monero. When a
payout batch has no unlocked balance to cover it, attempting transfer_split
fails noisily and, for batches, gets parked in needs_review for an operator.

Add a pre-flight unlocked-balance check that reverts the batch to 'pending'
(re-checked every new-block tick and batch interval) so it sends as soon as
outputs unlock — no manual intervention. Also classify insufficient-funds
RPC errors as pre-broadcast/safe-to-retry: single -> pending (not failed),
batch -> pending without an operator alert (not needs_review), since no tx
went on-chain and there's no double-pay risk.
2026-06-01 08:17:05 -04:00
jwinterm e6936f47d9 feat: network-aware currency label (sXMR), game name, and Smirk gating
- Currency display is now network-aware: XMR on a test network shows sXMR
  (stagenet) / tXMR (testnet) so it's clear the coins aren't mainnet; mainnet
  XMR shows XMR, Wownero stays WOW. A fork running mainnet automatically shows
  XMR. cryptoType (used for decimals/QR asset) is unchanged.
- Product name (Monerogue/Wownerogue) is derived from the currency and no
  longer hardcoded: the chat "Connected to <name> server" message, payment
  descriptions, and the verify page title all follow it.
- Smirk is forced off on any test network (stagenet/testnet) regardless of
  SMIRK_ENABLED, since Smirk only works on mainnet — so forks don't have to
  remember the env flag.

New shared helpers (gameNameFor, currencyLabelFor, isTestNetworkFor); exposed
via game_mode_info (currencyLabel, gameName). Full suite: 22 suites / 159 tests.
2026-05-31 15:57:59 -04:00
jwinterm d8d10728ba fix: gate direct (entry-fee) payouts behind DIRECT_PAYOUTS_ENABLED
completeGame only gated PAID_CREDITS payouts (on creditsPayoutEnabled); a
PAID_SINGLE win paid out unconditionally, so DIRECT_PAYOUTS_ENABLED=false did
NOT actually stop direct payouts — a no-payout instance (mainnet legitimacy)
would still send crypto on an entry-fee win. Caught during deploy verification.

- Add this.directPayoutEnabled (from DIRECT_PAYOUTS_ENABLED env / direct rule
  enabled in the config snapshot), mirroring creditsPayoutEnabled.
- completeGame now gates the PAID_SINGLE payout on directPayoutEnabled.
- The derived directPayoutsEnabled flags (getGameModeInfo / effective mode)
  and /api/game-modes PAID_SINGLE.payoutMultiplier now respect it (show 0 when
  off) instead of inferring from multipliers > 0.

Adds a regression test: a PAID_SINGLE win with directPayoutEnabled=false
queues no payout. Full suite: 22 suites / 159 tests.
2026-05-31 15:25:46 -04:00
jwinterm 988617289b feat: free-play + dual-leaderboard frontend
- Leaderboard panel: two boards via tabs — 🏅 Hall of Champions (paid) and
  🪙 Pleb (free) — default Champions; passes board= to /api/leaderboard and
  retitles per board.
- PaymentUI: the Free button now shows whenever free play is offered (instance
  free-only OR free-as-a-choice via freePlayEnabled), labelled "Play Free / no
  payout / Pleb leaderboard" on paid instances, and emits the play_free event
  (records game_mode=FREE). Replaces an emit of the dead 'enter_game' event.
- Server game_mode_info payload now includes freePlayEnabled so the client can
  show the choice.

Full suite: 22 suites / 158 tests.
2026-05-31 15:08:54 -04:00
jwinterm 0a1766ee2a feat: free-play choice + per-game dual leaderboard (backend)
Free-to-play alongside paid, with scores split per game between a Pleb board
(free) and the Hall of Champions (paid). Keys on the existing per-game
game_mode, so no new game columns.

- Leaderboard API: GET /api/leaderboard?board=pleb|champions|all. champions =
  game_mode IN (PAID_SINGLE,PAID_CREDITS), pleb = FREE. board is whitelisted.
  Migration 019 adds a supporting partial index.
- FREE_PLAY_ENABLED config: offer free play as an explicit choice even when the
  instance also sells credits/entry (always on when payments are off).
- play_free socket event -> handleAutoStart(socket, {free:true}); skips all
  payment/credits/payout-address gating and records the game as game_mode=FREE
  via processGameStart(..., {forceFree:true}) -> _processGameStartFree (no
  payment, no payout, counted).
- getPaymentOptionsForUser exposes a play_free option; /api/game-modes exposes
  freePlayEnabled.

Adds freePlay.test.js. Full backend suite: 22 suites / 158 tests.
2026-05-31 15:01:01 -04:00
jwinterm 7d3456f107 fix: dungeon reachability guard + per-substep monster collision
3.2 Reachability: a dungeon whose exit is unreachable from the entrance is
paid-but-unwinnable. generate() now BFS-checks entrance->exit and regenerates
if unreachable. Retries are deterministic (per-attempt ROT seed = seedInt +
attempt; the seeded rng stream keeps advancing), so verify-regeneration
replays the same attempts and reproduces the exact dungeon — the provably-fair
determinism from Phase 0.2 is preserved. The single-pass body is extracted to
_generateOnce and isReachable does the flood-fill.

3.3 Monster collision: with movesPerPlayerMove > 1 (the casino preset's 1.5x
speed) the monster takes multiple steps per turn, but collision was only
checked after the loop — so it could step onto the player's tile and off again
between checks, phasing through and missing the catch (player-favorable, erodes
the house edge). Collision is now checked after each sub-step.

Adds reachability tests (provablyFair.test.js) and monsterCollision.test.js.
Random block timing remains the untouched core mechanic. Full suite: 20 suites
/ 153 tests.
2026-05-31 14:08:45 -04:00
jwinterm 41676fa072 refactor: unify identity resolution on stable user_id (anon_token)
There were two identity systems: SessionManager keyed on the stable, UNIQUE
anon_token, and getOrCreateUser keyed on the mutable, non-unique socket_id.
The latter could create a duplicate "orphan" user row (or resolve the wrong
row) when the socket_id lookup raced the session's socket_id update —
desyncing credits and payouts between the two rows for one human.

- getOrCreateUser now resolves through SessionManager's stable identity first
  (anon_token -> users.id), re-selecting a fresh row by stable id, and only
  falls back to the legacy socket_id lookup when no session exists. The
  last-resort create now warns, since it indicates a missing session.
- gameModeManager.sessionManager is wired in SocketHandlers so the
  session-first path is active in production.
- anon_token is already UNIQUE (migration 003), so the stable-identity
  constraint already exists; no new migration needed.

Re-keying the in-memory activeGames map by user_id is deferred (invasive;
already mitigated by stamping dbUserId on the game in the disconnect fix).

Adds identityResolution.test.js proving no duplicate row is created when a
session exists. Full suite: 17 suites / 136 tests.
2026-05-31 10:15:08 -04:00
jwinterm f9c6ce59d2 fix: CHECK constraints on money-table statuses; cover processing in payout guard
A typo in a status string (e.g. 'complete' vs 'completed') silently produced
a money row no index/predicate matched, orphaning funds.

- Migration 017 adds idempotent NOT VALID CHECK constraints on payments.status,
  payments.payment_type and payouts.status. NOT VALID enforces all new writes
  without failing on any pre-existing rows; conname guards make it re-runnable.
- The in-code existing-payout guard in completeGame now also covers the
  'processing' status (matching the unique index from migration 015), so a
  duplicate win can't create a second payable row while a payout is mid-flight.

Full suite: 15 suites / 131 tests.
2026-05-31 09:57:56 -04:00
jwinterm 473a4091d4 fix: snapshot payout terms at game start (what-you-saw-is-what-you-get)
Payouts were computed at completion from LIVE config, so an admin changing
multipliers mid-game would change the payout of an already-in-progress game.

- Migration 016 adds payout_escape_amount/payout_treasure_amount (BIGINT) and
  the corresponding multipliers to the games table.
- Both game-start paths stamp the resolved payout amounts (computed from the
  config in effect at entry) onto the game row.
- completeGame pays from the snapshot, falling back to live calculation only
  for legacy games that predate the snapshot columns.

Adds payoutSnapshot.test.js proving a mid-game config change cannot alter an
in-flight payout. Full suite: 15 suites / 131 tests.
2026-05-31 09:55:45 -04:00
jwinterm 24aed355c6 refactor: exact BigInt money math via src/money/atomic.js
Money is integer atomic units, but payout and sum math ran on float64
(`Math.round(base * multiplier)`, `sum + Number(x)`), which silently loses
precision above 2^53 — a real risk for large/credit/XMR amounts.

- Add src/money/atomic.js: the single source of truth for atomic-unit math
  (toBig, toSafe, sum, add, mulByDecimal, format). mulByDecimal multiplies an
  atomic amount by a decimal multiplier using exact integer arithmetic with
  half-up rounding; toSafe narrows to a number only when exactly representable
  and otherwise returns a decimal string.
- calculatePayout now uses money.mulByDecimal/toSafe instead of float math.
- walletRPCService batch total + fee sum use money.sum instead of float reduce.

Adds moneyAtomic.test.js with property tests on amounts above 2^53 proving
exactness where the old float path drifted. Full suite: 14 suites / 129 tests.
2026-05-31 09:45:58 -04:00
jwinterm d07967c3d1 security: fix batch-payout tx_hash collision and double-pay risk
transfer_split sends one on-chain tx to many destinations, so payout rows
in a batch share a tx_hash. The old UNIQUE index on payouts.tx_hash made
the 2nd+ row fail to record, leaving funds sent but rows stranded in
'processing' and re-sendable by the retry service.

- Migration 015 drops the payouts tx_hash unique index (idempotency is
  enforced by idx_payouts_one_per_game) and extends that guard to cover
  'processing' so no second payable row can be inserted mid-flight.
- Batch completion now updates the whole batch in a single transaction via
  id = ANY(...), so rows can't be left half-completed. Per-user stats use
  BIGINT arithmetic in SQL (no JS float).
- Ambiguous batch failures are marked 'needs_review' (which the retry
  service does NOT pick up) instead of 'failed', preventing a double-send
  when transfer_split may have already broadcast. Operator is alerted.

Adds batchPayout.test.js covering the shared-tx_hash completion and the
needs_review failure path. Full suite: 12 suites / 115 tests.
2026-05-31 09:26:14 -04:00
jwinterm 3e6e3b858b security: fix underpayment exploit and make provably-fair real
Phase 0.1 — Underpayment confirmation bug (CRITICAL):
Payment confirmation gated on `status.confirmed`, which checkPaymentStatus
sets for ANY incoming tx regardless of amount, so sending a single atomic
unit granted a full game/credits. Now gate on `status.complete` (received
>= expected). Underpayers are warned once and the payment stays pending so
a top-up can complete it. Persist `received_amount` for single-game and
credits confirmations for reconciliation. Adds an underpayment regression
test.

Phase 0.2 — Provably-fair was decorative (CRITICAL):
The committed seed had zero effect on the dungeon (created seededRNG was
unused; generation used Math.random + a globally-seeded ROT.RNG). Thread a
per-game seeded RNG derived from the committed seed through dungeon
generation (reseed the Digger via seedToInt + replace all Math.random) and
monster movement (seededShuffle). Add DungeonGenerator.regenerateFromSeed
and layoutFingerprint; /api/verify now regenerates the dungeon from the
seed and returns a fingerprint a player can independently reproduce. Adds a
determinism test suite.

Full suite: 11 suites / 113 tests passing.

Plan and progress tracked in docs/REMEDIATION_PLAN.md.
2026-05-31 09:14:21 -04:00
jwinterm a61851b0f7 add smirk payments and leaderboard and diff tuning 2026-02-20 13:54:36 -05:00
jwinterm 27660d6948 Fix rewards, make tx hash clickable, add win payout notification, and add entry choice modal 2026-02-20 12:34:21 -05:00
jwinterm da6763886a fix credits display p3 and update .env.example 2026-02-20 11:11:51 -05:00
jwinterm 5776c58357 fix credits package bug and improve stale payment logic 2026-02-19 07:05:15 -05:00
jwinterm bc55f0355e fix edge cases and tests 2026-02-19 06:15:45 -05:00
jwinterm ec3caabf1c feat: initial batch payout logic and output splitting script added 2026-02-18 07:30:46 -05:00
jwinterm bf5938c366 fix game entry and payout bugs 2026-02-18 06:35:32 -05:00
jwinterm 320d1a3b1f fix admin panel and credits package 2026-02-17 20:02:22 -05:00
jwinterm 6cf3961da0 feat: add smirk toggle and more admin capabilities 2026-02-02 08:42:39 -05:00
jwinterm 9a53df4a35 Fix critical double pay bugs 2026-02-01 16:38:51 -05:00
jwinterm 2286d8deff Feat: add smirk login
Fix: add security hardening and transaction database commits
2026-01-28 17:21:53 -05:00
jwinterm 38a7e58e96 fix: payment flow 2026-01-13 18:49:59 -05:00
jwinterm ce58a33347 Fix payment bugs, add comprehensive tests, remove dead code
Payment Fixes:
- Prevent credits going negative with WHERE credits >= $1 check
- completeGame now uses recorded payment_mode from game record instead of global gameMode
- Add duplicate payout prevention by checking for existing payouts before processing

Dead Code Cleanup:
- Remove unused MoneroPayService (WalletRPCService is the actual service)
- Remove legacy MoneroPay references from comments

Tests:
- Add 50 comprehensive payment/payout tests (payment_payout_comprehensive.test.js)
- Expand WalletRPCService tests (4 -> 31 tests)
- Total: 106 tests passing across 10 test suites

Documentation:
- Update TODO.md with recent fixes and correct test counts
- Update migration comments to reflect actual wallet service used
2026-01-13 16:45:11 -05:00
jwinterm 00f8057cb5 Fix credits mode payment processing and payout tracking
- Add payout database records: processPayout now inserts into payouts
  table with tx_hash, fee, status, and updates user statistics
- Fix credits deduction: QueueManager game start methods now properly
  call processGameStart() to deduct credits (was being skipped)
- Fix credits_update emission: moved from GameModeManager (no io access)
  to socketHandlers/queueManager where io is available
- Add payment recovery: users who disconnect during payment confirmation
  now get credits recovered automatically on reconnect
- Remove dead handlePaymentDetected method that referenced undefined this.db
2026-01-02 14:19:11 -05:00
jwinterm aec1c71a8c add early entry and help screens for new users 2025-12-26 10:21:25 -05:00
jwinterm f1c86a084b Fix payout calculation - update defaults and add debug logging 2025-12-24 21:00:22 -05:00
jwinterm b062a1a5fa debug payouts 2025-12-24 20:54:07 -05:00
jwinterm 2c781ec043 fix payout calcs 2025-12-24 20:03:26 -05:00