feat(economy): add /bank vault (rob-proof storage) + net-worth leaderboard

A strategic counterpart to /rob: coins moved to the bank are safe from /rob and
/heist (which only touch liquid balance) but earn no Bot Farm interest and can't
be spent, gambled or given until withdrawn - the deliberate trade-off against
keeping coins liquid.

- New bank.py (do_deposit/do_withdraw), bank_balance schema field.
- /bank (view), /deposit, /withdraw commands ('all' supported), with db_error
  handling; /balance shows the vault when non-zero.
- Coins leaderboard and /status money-supply now count net worth
  (balance + bank_balance), so banking never hides you from the board or the
  supply metric.
- Season reset wipes bank_balance too (no cross-season wealth hiding).

10 tests: deposit/withdraw math, guards, coin conservation, rob cannot touch the
vault, and net-worth accounting on the leaderboard + stats.

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:53:14 +03:00
parent 417617175e
commit 42bbdbd93d
12 changed files with 264 additions and 10 deletions

View File

@@ -7,11 +7,17 @@ from . import house
from .levels import get_level
def _net_worth(r: dict) -> int:
"""Coins that count toward wealth: liquid balance + banked vault."""
return (r.get("balance", 0) or 0) + (r.get("bank_balance", 0) or 0)
async def get_leaderboard(top_n: int | None = 10) -> list[tuple[str, int]]:
"""Return top_n (user_id_str, balance) pairs sorted descending."""
"""Return top_n (user_id_str, net_worth) pairs sorted descending.
Net worth = balance + bank_balance, so banking coins does not hide them."""
records = await pb_client.list_all_records()
result = sorted(
((r["user_id"], r.get("balance", 0)) for r in records if r.get("user_id")),
((r["user_id"], _net_worth(r)) for r in records if r.get("user_id")),
key=lambda x: x[1],
reverse=True,
)
@@ -97,8 +103,8 @@ async def get_all_leaderboards() -> dict[str, list[tuple]]:
return sorted(users, key=keyfn, reverse=True)
return {
"coins": [(r["user_id"], r.get("balance", 0))
for r in desc(lambda r: r.get("balance", 0))],
"coins": [(r["user_id"], _net_worth(r))
for r in desc(_net_worth)],
"exp": [(r["user_id"], r.get("exp", 0), get_level(r.get("exp", 0)))
for r in desc(lambda r: r.get("exp", 0))],
"season": [(r["user_id"], r.get("season_total_exp", 0), r.get("prestige_level", 0))
@@ -125,10 +131,11 @@ async def get_economy_stats() -> dict[str, int]:
uid = r.get("user_id")
if not uid:
continue
bal = r.get("balance", 0) or 0
total += bal
# Banked coins are still part of the money supply.
worth = (r.get("balance", 0) or 0) + (r.get("bank_balance", 0) or 0)
total += worth
if uid == house_id:
house_balance = bal
house_balance = worth
else:
player_count += 1
return {