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:
@@ -6,7 +6,10 @@ import random
|
||||
|
||||
from ..pb_client import DatabaseError
|
||||
from ..emoji import EMOJI as E
|
||||
from .store import _commit, _is_jailed, _locked_by, _log, _txn, get_user
|
||||
from .store import (
|
||||
_commit, _is_jailed, _locked_by, _log, _txn, add_pending_wager,
|
||||
clear_pending_wager, get_user,
|
||||
)
|
||||
from .house import _credit_house
|
||||
|
||||
|
||||
@@ -105,6 +108,7 @@ async def do_rps_pvp_deposit(user_id: int, bet: int) -> dict:
|
||||
return {"ok": False, "reason": "insufficient"}
|
||||
user["balance"] -= bet
|
||||
user["total_wagered"] = user.get("total_wagered", 0) + bet
|
||||
add_pending_wager(user, "rps", bet) # escrow survives a restart
|
||||
try:
|
||||
await _commit(user_id, user)
|
||||
except DatabaseError:
|
||||
@@ -125,6 +129,7 @@ async def do_rps_pvp_payout(winner_id: int, bet: int) -> dict:
|
||||
user["lifetime_earned"] = user.get("lifetime_earned", 0) + bet
|
||||
user["biggest_win"] = max(user.get("biggest_win", 0), bet)
|
||||
user["peak_balance"] = max(user.get("peak_balance", 0), user["balance"])
|
||||
clear_pending_wager(user) # winner's escrow settled
|
||||
try:
|
||||
await _commit(winner_id, user)
|
||||
except DatabaseError:
|
||||
@@ -133,6 +138,23 @@ async def do_rps_pvp_payout(winner_id: int, bet: int) -> dict:
|
||||
return {"ok": True, "balance": user["balance"]}
|
||||
|
||||
|
||||
@_locked_by(0)
|
||||
async def do_rps_pvp_forfeit(loser_id: int) -> dict:
|
||||
"""Release the loser's escrow marker without refunding - their stake was paid
|
||||
to the winner as part of the 2*bet payout. Without this the loser's
|
||||
pending_wager would linger and be wrongly refunded on the next restart."""
|
||||
try:
|
||||
user = await get_user(loser_id)
|
||||
except DatabaseError:
|
||||
return {"ok": False, "reason": "db_error"}
|
||||
clear_pending_wager(user)
|
||||
try:
|
||||
await _commit(loser_id, user)
|
||||
except DatabaseError:
|
||||
return {"ok": False, "reason": "db_error"}
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@_locked_by(0)
|
||||
async def do_rps_pvp_refund(user_id: int, bet: int) -> dict:
|
||||
"""Refund a previously escrowed bet (tie / timeout / cancel)."""
|
||||
@@ -142,6 +164,7 @@ async def do_rps_pvp_refund(user_id: int, bet: int) -> dict:
|
||||
return {"ok": False, "reason": "db_error"}
|
||||
user["balance"] = user.get("balance", 0) + bet
|
||||
user["total_wagered"] = max(0, user.get("total_wagered", 0) - bet)
|
||||
clear_pending_wager(user) # escrow returned
|
||||
try:
|
||||
await _commit(user_id, user)
|
||||
except DatabaseError:
|
||||
@@ -248,6 +271,9 @@ async def do_blackjack_bet(user_id: int, bet: int) -> dict:
|
||||
if user["balance"] < bet:
|
||||
return {"ok": False, "reason": "insufficient", "balance": user["balance"]}
|
||||
user["balance"] -= bet
|
||||
# Escrow the stake in the same commit (accumulates across double/split), so a
|
||||
# restart mid-hand refunds it via reconcile_pending_wagers instead of eating it.
|
||||
add_pending_wager(user, "blackjack", bet)
|
||||
try:
|
||||
await _commit(user_id, user)
|
||||
except DatabaseError:
|
||||
@@ -279,6 +305,7 @@ async def do_blackjack_payout(user_id: int, payout: int, total_invested: int = 0
|
||||
elif net < 0:
|
||||
user["lifetime_lost"] = user.get("lifetime_lost", 0) + abs(net)
|
||||
user["biggest_loss"] = max(user.get("biggest_loss", 0), abs(net))
|
||||
clear_pending_wager(user) # hand settled - release the escrow marker
|
||||
try:
|
||||
await _commit(user_id, user)
|
||||
except DatabaseError:
|
||||
|
||||
Reference in New Issue
Block a user