"""Bank heist: group robbery of the house.""" from __future__ import annotations import random from .. import pb_client from ..pb_client import DatabaseError from . import house from .store import HEIST_JAIL, _commit, _now, _txn, _user_lock, get_user from .house import _credit_house, _refund_house_safe, _refund_user_safe # --------------------------------------------------------------------------- # /heist # --------------------------------------------------------------------------- async def do_heist_check(user_id: int) -> dict: """Check whether a user is eligible to join a heist.""" try: user = await get_user(user_id) except DatabaseError: return {"ok": False, "reason": "db_error"} if user.get("eco_banned"): return {"ok": False, "reason": "banned"} if jail := _is_jailed(user): return {"ok": False, "reason": "jailed", "remaining": jail} return {"ok": True} async def do_heist_resolve(user_ids: list[int], success: bool) -> dict: """Apply heist outcome to all participants. On win, steals from house. Per-user commit failures attempt to compensate the house so the economy stays balanced. If compensation also fails, a CRITICAL log is emitted. """ now = _now() payout_each = 0 failed_users: list[int] = [] if success and house.HOUSE_ID is not None: try: house = await get_user(house.HOUSE_ID) pct = random.uniform(0.20, 0.55) total = max(300, int(house["balance"] * pct)) payout_each = total // len(user_ids) # Atomic decrement (capped at the balance we read) instead of a full # record commit, so concurrent _credit_house increments aren't lost. debit = min(total, house["balance"]) if debit > 0: await pb_client.update_record(house["_pb_id"], {"balance-": debit}) # type: ignore[typeddict-item] except DatabaseError: return {"ok": False, "reason": "db_error"} _txn("HEIST_HOUSE", change=f"-{debit}", house_bal=house["balance"] - debit) for uid in user_ids: async with _user_lock(uid): try: user = await get_user(uid) except DatabaseError: failed_users.append(uid) if success and payout_each > 0: await _refund_house_safe(payout_each, "heist_win_compensate", uid) continue user["last_heist"] = now.isoformat() user["heists_joined"] = user.get("heists_joined", 0) + 1 fine_credited = False if success: user["balance"] += payout_each user["heists_won"] = user.get("heists_won", 0) + 1 user["lifetime_earned"] = user.get("lifetime_earned", 0) + payout_each user["peak_balance"] = max(user.get("peak_balance", 0), user["balance"]) _txn("HEIST_WIN", user=uid, change=f"+{payout_each}", bal=user["balance"]) else: fine = max(150, min(1000, int(user["balance"] * 0.15))) user["balance"] = max(0, user["balance"] - fine) user["jailed_until"] = (now + HEIST_JAIL).isoformat() user["jailbreak_used"] = False user["times_jailed"] = user.get("times_jailed", 0) + 1 user["lifetime_lost"] = user.get("lifetime_lost", 0) + fine _txn("HEIST_FAIL", user=uid, fine=f"-{fine}", jailed_until=user["jailed_until"], bal=user["balance"]) if fine > 0: try: await _credit_house(fine) fine_credited = True except DatabaseError: pass # user commit will still be attempted; if both fail, no economy effect try: await _commit(uid, user) except DatabaseError: failed_users.append(uid) if success and payout_each > 0: await _refund_house_safe(payout_each, "heist_win_compensate", uid) elif not success and fine_credited: await _refund_user_safe(house.HOUSE_ID, fine if 'fine' in locals() else 0, "heist_fail_compensate", uid) return {"ok": True, "payout_each": payout_each, "success": success, "failed_users": failed_users}