feat(economy): persist interactive-game stakes so a restart can't eat them

Blackjack and RPS PvP deduct a stake up front and held it only in an in-memory
View - a restart mid-hand lost the coins. Now the stake is escrowed on the user
record (new pending_wager JSON field) in the SAME commit as the deduction, and:

- do_blackjack_bet accumulates the escrow (covers double/split); do_blackjack_payout
  clears it on settlement (incl. the 0-payout loss/timeout paths).
- do_rps_pvp_deposit records it; do_rps_pvp_payout/refund clear it, and a new
  do_rps_pvp_forfeit clears the loser's marker (their stake went to the winner).
- reconcile_pending_wagers() runs on startup (on_ready) and refunds any stake left
  escrowed by an interrupted game. It's idempotent and locks per user.

Schema: pending_wager auto-types as json via sync_pb_schema. Tests cover the full
escrow lifecycle, loser forfeit, and idempotent reconciliation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
This commit is contained in:
Rene Arumetsa
2026-09-04 02:37:43 +03:00
parent f72d72b355
commit 8481003edf
5 changed files with 202 additions and 1 deletions

10
bot.py
View File

@@ -441,6 +441,16 @@ async def on_ready():
# Re-schedule any reminder tasks lost on restart
await _restore_reminders()
# Refund stakes escrowed by interactive games (blackjack/RPS PvP) that a
# restart interrupted, so a mid-hand crash never eats a player's coins.
try:
refunded = await economy.reconcile_pending_wagers()
if refunded:
total = sum(amt for _, amt, _ in refunded)
log.info("Reconciled %d interrupted wager(s), refunded %d coins", len(refunded), total)
except Exception:
log.exception("Pending-wager reconciliation failed")
# Notify the channel where /restart was triggered
if _RESTART_FILE.exists():
try: