forked from sass/tipibot
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:
86
tests/test_bank.py
Normal file
86
tests/test_bank.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Tests for the bank vault: rob-proof storage and net-worth accounting."""
|
||||
|
||||
from core import economy
|
||||
|
||||
from conftest import run
|
||||
|
||||
UID = 6161
|
||||
ROBBER = 6162
|
||||
|
||||
|
||||
def _fund(fake_pb, uid: int, balance: int, bank: int = 0) -> None:
|
||||
run(economy.get_user(uid))
|
||||
rec = fake_pb.record_for(uid)
|
||||
rec["balance"] = balance
|
||||
rec["bank_balance"] = bank
|
||||
|
||||
|
||||
class TestDepositWithdraw:
|
||||
def test_deposit_moves_liquid_to_bank(self, fake_pb):
|
||||
_fund(fake_pb, UID, 1000)
|
||||
res = run(economy.do_deposit(UID, 400))
|
||||
assert res["ok"] and res["balance"] == 600 and res["bank"] == 400
|
||||
rec = fake_pb.record_for(UID)
|
||||
assert rec["balance"] == 600 and rec["bank_balance"] == 400
|
||||
|
||||
def test_withdraw_moves_bank_to_liquid(self, fake_pb):
|
||||
_fund(fake_pb, UID, 100, bank=500)
|
||||
res = run(economy.do_withdraw(UID, 300))
|
||||
assert res["ok"] and res["balance"] == 400 and res["bank"] == 200
|
||||
|
||||
def test_deposit_more_than_liquid_rejected(self, fake_pb):
|
||||
_fund(fake_pb, UID, 100)
|
||||
res = run(economy.do_deposit(UID, 500))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
assert fake_pb.record_for(UID)["balance"] == 100 # unchanged
|
||||
|
||||
def test_withdraw_more_than_bank_rejected(self, fake_pb):
|
||||
_fund(fake_pb, UID, 0, bank=100)
|
||||
res = run(economy.do_withdraw(UID, 500))
|
||||
assert not res["ok"] and res["reason"] == "insufficient"
|
||||
assert fake_pb.record_for(UID)["bank_balance"] == 100
|
||||
|
||||
def test_nonpositive_rejected(self, fake_pb):
|
||||
_fund(fake_pb, UID, 1000, bank=1000)
|
||||
assert run(economy.do_deposit(UID, 0))["reason"] == "invalid"
|
||||
assert run(economy.do_withdraw(UID, -5))["reason"] == "invalid"
|
||||
|
||||
def test_banned_rejected(self, fake_pb):
|
||||
_fund(fake_pb, UID, 1000)
|
||||
fake_pb.record_for(UID)["eco_banned"] = True
|
||||
assert run(economy.do_deposit(UID, 100))["reason"] == "banned"
|
||||
|
||||
def test_round_trip_conserves_coins(self, fake_pb):
|
||||
_fund(fake_pb, UID, 1000)
|
||||
run(economy.do_deposit(UID, 700))
|
||||
run(economy.do_withdraw(UID, 700))
|
||||
rec = fake_pb.record_for(UID)
|
||||
assert rec["balance"] == 1000 and rec["bank_balance"] == 0
|
||||
|
||||
|
||||
class TestRobProof:
|
||||
def test_rob_cannot_touch_banked_coins(self, fake_pb, monkeypatch):
|
||||
# Target keeps everything banked, only a little liquid (< rob threshold).
|
||||
_fund(fake_pb, UID, 50, bank=100_000)
|
||||
_fund(fake_pb, ROBBER, 1000)
|
||||
# Target has < 100 liquid, so a rob is rejected as "broke" - the vault is
|
||||
# invisible to /rob (which only reads balance).
|
||||
res = run(economy.do_rob(ROBBER, UID))
|
||||
assert not res["ok"] and res["reason"] == "broke"
|
||||
assert fake_pb.record_for(UID)["bank_balance"] == 100_000 # untouched
|
||||
|
||||
|
||||
class TestNetWorth:
|
||||
def test_leaderboard_counts_bank(self, fake_pb):
|
||||
_fund(fake_pb, UID, 100, bank=900) # net worth 1000
|
||||
_fund(fake_pb, ROBBER, 500, bank=0) # net worth 500
|
||||
board = dict((uid, worth) for uid, worth in run(economy.get_leaderboard(top_n=None)))
|
||||
assert board[str(UID)] == 1000
|
||||
assert board[str(ROBBER)] == 500
|
||||
|
||||
def test_economy_stats_counts_bank(self, fake_pb, monkeypatch):
|
||||
monkeypatch.setattr(economy.house, "HOUSE_ID", None)
|
||||
_fund(fake_pb, UID, 100, bank=900)
|
||||
stats = run(economy.get_economy_stats())
|
||||
assert stats["total_coins"] == 1000
|
||||
assert stats["player_coins"] == 1000
|
||||
Reference in New Issue
Block a user