Commit Graph
36 Commits
Author SHA1 Message Date
jwinterm ebf14560dd feat(tavern): unified global chat with persistent history in the tavern
The tavern used ephemeral, room-scoped chat with no backlog, so entering it
showed a blank log. Wire it to the shared global chat provider instead:

- TavernManager takes a globalChatProvider (the lobby's chat provider) and
  publishes tavern messages to the 'global' scope, so tavern + lobby share ONE
  conversation that persists to Postgres and (when NOSTR_CHAT_ENABLED) fans out
  over relay.smirk.cash. Falls back to the old tavern-scoped ephemeral provider
  when none is injected.
- On join, the tavern sends the recent global backlog (chat_history) to the
  joiner so history shows immediately.
- tavern.html: render chat_history on join and accept global-scope broadcasts
  (not just this room's), so lobby<->tavern messages flow both ways.

Tests: +3 (join sends backlog, chat routes to global scope, legacy fallback
stays tavern-scoped). Suite 55 suites / 394 tests green.
2026-07-10 19:54:38 -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 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 d5df5dbd12 refactor: remove dead move handler + stale test scratch files
4.2 (partial): Delete the dead handlePlayerMove socket handler and its
orphaned playerMoveTimestamps map + cleanup. It was never registered (the live
move path is MovementManager.handleMove) but, if ever wired, it double-stepped
the monster and bypassed the escape/collision event handling — a loaded
footgun. No behaviour change.

4.5: Delete 14 stale non-jest files from test/ (5 scratch .js scripts + 9
.html debug pages) — they contain no jest constructs and are referenced
nowhere. test/ is now exclusively the 21 real jest suites.

Full suite: 21 suites / 155 tests.
2026-05-31 14:31:10 -04:00
jwinterm d2ff9c27ba fix: tiny anti-instant-death grace on block timeout (random timing preserved)
Random block timing is the core mechanic and is intentionally NOT softened —
early entry is a knowingly-risky bet. The only change is a small configurable
grace (GAME_START_GRACE_MS, default 2000ms, 0 to disable) so a block landing
in the same instant a game starts can't kill the player before the dungeon
even renders or before their first move is possible (100ms move cooldown).

(Replaces an earlier over-reaching "uniform time budget" idea that would have
defeated the random-timing mechanic.)
2026-05-31 13:52:51 -04:00
jwinterm 895aa2af3d fix: input validation + memory-leak/concurrency hardening
- register_client now validates the client-supplied clientId (type, length,
  charset) before it enters clientSocketMap as both key and value, closing a
  map-poisoning / mapping-impersonation vector.
- mempoolNotified is now a Map with real TTL eviction owned by PaymentHandlers
  (the actual dedup set). The dead duplicate Set in SocketHandlers and its
  random-10%-per-cycle eviction (which could drop live entries) are removed;
  addresses are also evicted promptly when monitoring stops.
- The SessionManager `sessions` cache is evicted on disconnect (removeSocket,
  called after the suspend logic reads it) so it no longer grows unbounded
  with every socket ever seen.
- sessionManager.dispose(), suspendedGameManager.cleanup() and
  paymentHandlers.dispose() are wired into SocketHandlers.shutdown() so their
  timers/maps don't leak on shutdown.
- checkGamesTimeout snapshots activeGames before iterating and awaits each
  game-over sequentially (was mutating the Map during forEach and firing
  async game-overs concurrently, which could skip games / double-process).

Adds inputValidation.test.js. Full suite: 19 suites / 149 tests.
2026-05-31 13:46:53 -04:00
jwinterm 448cb2f528 fix: reconnect-proof rate limiting (stable id + real client IP)
Socket-event rate limits keyed on socket.id (new on every reconnect) and
passed NO ip argument, so the "IP-limited" prefixes never actually applied —
reconnecting reset every limit, and chat had no IP limit at all.

- New rateLimitContext helper resolves a STABLE identity (anon_token -> id,
  falling back to socket id) and the real client IP (X-Forwarded-For honoured
  only when TRUST_PROXY=true, else handshake address — no spoofing on direct
  exposure).
- game:start, address:set and chat:message now pass (stableId, action, ip);
  connection:new uses the real IP. 'chat:' added to IP-limited prefixes.
- Express 'trust proxy' enabled when TRUST_PROXY=true; documented in
  .env.example. Aligned the contradictory default limit values with the
  production override.

Adds rateLimit.test.js (reconnect can't bypass, IP backstop for fresh
identities, XFF gating). Full suite: 18 suites / 143 tests.
2026-05-31 10:21:28 -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 5c546a1473 fix: recover unconsumed single-game payments; make game preservation deterministic
Two disconnect-related money/UX gaps:

1. A confirmed single_game payment whose game never started (user paid, then
   disconnected) was money-in-nothing-given with no recovery path. recover-
   PendingPayments now detects confirmed single_game payments with no linked
   games row and grants the equivalent credits — idempotently (per-payment
   reason key, row-locked re-check of both the game link and prior recovery).

2. In-progress games could be dropped on disconnect when a fresh getBySocket
   lookup failed. The stable dbUserId is now stamped on the game at creation,
   so suspend/restore keys on it directly without a failure-prone disconnect-
   time lookup. Any game (paid or free) is preserved/reconnectable whenever a
   session exists; the remaining drop-branch is only the true no-session edge
   (e.g. DB unavailable at connect), which is logged for audit rather than
   masked.

Adds singleGameRecovery.test.js. Full suite: 13 suites / 119 tests.
2026-05-31 09:36:09 -04: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 ee5a037b84 fix credits logic 2026-02-20 09:21:13 -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 758d67e72e fix: table lookup and move some messages to debug mode 2026-01-14 08:17:47 -05:00
jwinterm 79cbc4b9e6 fix: Open SHOP modal on START GAME if payout address already set and no credits available 2026-01-13 17:58:14 -05:00
jwinterm e8e0405485 fix: START GAME shows shop modal instead of error when payment required
When both payment modes are enabled (mixed mode) and user has no credits
or confirmed payment, the 'choose_payment' action was falling through to
the error case. Now 'choose_payment' triggers the payment options modal
just like 'make_payment' and 'purchase_credits' do.
2026-01-13 17:46:59 -05:00
jwinterm 404f5692d8 feat: Game reconnection/resume support
- Created SuspendedGameManager to preserve game state when users disconnect
- Games are keyed by DB userId (persistent) instead of ephemeral socketId
- handleDisconnect now suspends games for 5 minutes instead of deleting
- handleConnection restores suspended games on reconnect with session token
- Payment monitoring is restored for pending payments on reconnect
- Added hasActiveMonitoring() method to PaymentHandlers
- Queue entries preserved for users with active sessions/payments
- Updated TODO.md with reconnection feature documentation

Users in dungeon or awaiting payment can now close/refresh browser and
seamlessly resume within 5 minutes when reconnecting.
2026-01-13 17:06:35 -05:00
jwinterm 251ace4635 Fix: make reconnect more robust and make spectate window re-openable 2026-01-07 17:44:47 -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 0ff95d3142 fix payment modal p2 2025-12-24 19:51:32 -05:00
jwinterm 7f91d83308 fix monster bug and let spectators join pending games 2025-12-22 11:25:22 -05:00
jwinterm 24f46fa35e fix chat and payments and add spectator mode 2025-12-22 10:36:46 -05:00
jwinterm deca4a4685 add db commands and test payout (worked!) 2025-11-23 13:33:04 -05:00
jwinterm 4e7a773496 add address modal and try to fix payout 2025-09-29 17:40:59 -04:00
jwinterm 9a4911f410 fix audio and add new audio and fix qr and db index 2025-09-28 10:43:48 -04:00
jwinterm f8241dd309 Continue working on payment logic 2025-09-27 11:21:11 -04:00
jwinterm e3915dcde2 Begin payment logic and tests 2025-09-27 08:45:22 -04:00
jwinterm 4ad063f735 Get back into it 2025-09-24 14:06:42 -04:00
jwinterm bd97d9d848 First (untested) payment and db setup 2025-06-01 14:17:29 -04:00
jwinterm f02e16272e refactor frontend and fix debug mode 2025-05-29 13:40:44 -04:00
jwinterm 49166b00c6 quicksave 2025-05-26 20:23:41 -04:00
jwinterm 50ccf28a04 refactor backend folder structure 2025-05-26 20:04:25 -04:00