Implements the moderation and consent obligations a social platform carries, so the web version and any client that speaks to it enforce the same rules. Moderation core (services/moderation/, database/moderation.py): a reportable target registry, the content filter and its choke points, the report queue with atomic resolution, enforcement actions, consent tracking, maturity gating, and account deletion with a grace window. Surfaces: POST /reports plus the member report list, /admin/moderation and the per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and account deletion under /profile, the report button and dialog partials, the maturity gate, and the moderation stylesheet and ReportDialog client. Every user-generated surface stays reportable by construction: new content tables are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a reason, and the registry test fails the suite on anything left unclassified. Docs: community guidelines, content moderation, intellectual property, privacy, terms, contact, and the admin-only moderation operations page, plus the moderation API group and the Devii moderation actions. Compliance record: applecomp.md is the requirement register, applechanges.md the gap analysis against this codebase, and appleimpl.md the implementation design they resolve to. Tests cover the report flow, admin moderation, consent, account deletion, terms acceptance, workspaces, and the registry invariant across the unit, api, and e2e tiers.
1181 lines
38 KiB
Python
1181 lines
38 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import pytest
|
|
|
|
from devplacepy.database import get_table
|
|
from devplacepy.services.game import economy, store
|
|
from devplacepy.services.game.store import GameError
|
|
|
|
|
|
def _user(username):
|
|
users = get_table("users")
|
|
row = users.find_one(username=username)
|
|
if not row:
|
|
users.insert(
|
|
{
|
|
"uid": f"uid-{username}",
|
|
"username": username,
|
|
"terms_version": "1",
|
|
"role": "Member",
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
)
|
|
row = users.find_one(username=username)
|
|
return row
|
|
|
|
|
|
def _clear_game_caches():
|
|
from devplacepy.services.game.store import farm as farm_store
|
|
from devplacepy.services.game.store import market as market_store
|
|
|
|
farm_store._leaderboard_cache.clear()
|
|
farm_store._board_cache.clear()
|
|
market_store._saturation_cache.clear()
|
|
|
|
|
|
def _reset(username, coins=100000):
|
|
user = _user(username)
|
|
farm = store.get_farm(user["uid"])
|
|
if farm:
|
|
get_table("game_plots").delete(farm_uid=farm["uid"])
|
|
get_table("game_quests").delete(farm_uid=farm["uid"])
|
|
get_table("game_farms").delete(uid=farm["uid"])
|
|
get_table("game_steals").delete(thief_uid=user["uid"])
|
|
get_table("game_steals").delete(owner_uid=user["uid"])
|
|
get_table("game_cosmetics").delete(user_uid=user["uid"])
|
|
get_table("game_market_ticks").delete()
|
|
get_table("game_treasury").delete()
|
|
_clear_game_caches()
|
|
farm = store.ensure_farm(user["uid"])
|
|
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
|
return user
|
|
|
|
|
|
def _set(user, **fields):
|
|
farm = store.get_farm(user["uid"])
|
|
get_table("game_farms").update({"uid": farm["uid"], **fields}, ["uid"])
|
|
|
|
|
|
def _warp(user):
|
|
farm = store.get_farm(user["uid"])
|
|
past = (datetime.now(timezone.utc) - timedelta(seconds=5)).isoformat()
|
|
for plot in store.get_plots(farm["uid"]):
|
|
if plot.get("crop_key"):
|
|
get_table("game_plots").update(
|
|
{"uid": plot["uid"], "ready_at": past}, ["uid"]
|
|
)
|
|
|
|
|
|
def _remaining(user, slot=0):
|
|
farm = store.get_farm(user["uid"])
|
|
data = store.serialize_farm(farm, viewer=user, owner=user)
|
|
return data["plots"][slot]["remaining_seconds"]
|
|
|
|
|
|
def _coins(user):
|
|
return int(store.get_farm(user["uid"])["coins"])
|
|
|
|
|
|
# --- ensure / starting state -------------------------------------------------
|
|
|
|
|
|
def test_ensure_farm_starting_state(local_db):
|
|
user = _reset("unit_a", coins=economy.STARTING_COINS)
|
|
farm = store.get_farm(user["uid"])
|
|
assert farm["plot_count"] == economy.STARTING_PLOTS
|
|
plots = store.get_plots(farm["uid"])
|
|
assert len(plots) == economy.STARTING_PLOTS
|
|
assert all(not plot["crop_key"] for plot in plots)
|
|
|
|
|
|
def test_ensure_farm_idempotent(local_db):
|
|
user = _reset("unit_a")
|
|
first = store.ensure_farm(user["uid"])
|
|
second = store.ensure_farm(user["uid"])
|
|
assert first["uid"] == second["uid"]
|
|
assert len(store.get_plots(first["uid"])) == economy.STARTING_PLOTS
|
|
|
|
|
|
# --- plant -------------------------------------------------------------------
|
|
|
|
|
|
def test_plant_success_deducts_cost(local_db):
|
|
user = _reset("unit_a")
|
|
result = store.plant(user, 0, "shell")
|
|
assert result["spent"] == economy.crop_for("shell").cost
|
|
assert _coins(user) == 100000 - economy.crop_for("shell").cost
|
|
plot = store.get_plots(store.get_farm(user["uid"])["uid"])[0]
|
|
assert plot["crop_key"] == "shell"
|
|
|
|
|
|
def test_plant_insufficient_coins(local_db):
|
|
user = _reset("unit_a", coins=0)
|
|
with pytest.raises(GameError):
|
|
store.plant(user, 0, "shell")
|
|
|
|
|
|
def test_plant_occupied_plot(local_db):
|
|
user = _reset("unit_a")
|
|
store.plant(user, 0, "shell")
|
|
with pytest.raises(GameError):
|
|
store.plant(user, 0, "python")
|
|
|
|
|
|
def test_plant_locked_crop(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.plant(user, 0, "rust")
|
|
|
|
|
|
def test_plant_unknown_crop(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.plant(user, 0, "does_not_exist")
|
|
|
|
|
|
def test_plant_nonexistent_plot(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.plant(user, 99, "shell")
|
|
|
|
|
|
def test_discount_perk_lowers_plant_cost(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, perk_discount=3)
|
|
result = store.plant(user, 0, "python")
|
|
crop = economy.crop_for("python")
|
|
assert result["spent"] == economy.effective_plant_cost(crop, 3)
|
|
assert result["spent"] < crop.cost
|
|
|
|
|
|
def test_growth_perk_shortens_build(local_db):
|
|
plain = _reset("unit_a")
|
|
store.plant(plain, 0, "python")
|
|
base = _remaining(plain)
|
|
boosted = _reset("unit_b")
|
|
_set(boosted, perk_growth=8)
|
|
store.plant(boosted, 0, "python")
|
|
assert _remaining(boosted) < base
|
|
|
|
|
|
# --- harvest -----------------------------------------------------------------
|
|
|
|
|
|
def test_harvest_success_awards_coins_and_xp(local_db):
|
|
user = _reset("unit_a")
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
before = _coins(user)
|
|
result = store.harvest(user, 0)
|
|
crop = economy.crop_for("shell")
|
|
expected = economy.realizable_harvest_coins(crop, golden=result["golden"])
|
|
assert result["coins"] == expected
|
|
assert result["xp"] == crop.reward_xp
|
|
assert _coins(user) == before + expected
|
|
assert store.get_farm(user["uid"])["total_harvests"] == 1
|
|
|
|
|
|
def test_harvest_empty_plot(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.harvest(user, 0)
|
|
|
|
|
|
def test_harvest_unripe_build(local_db):
|
|
user = _reset("unit_a")
|
|
store.plant(user, 0, "python")
|
|
with pytest.raises(GameError):
|
|
store.harvest(user, 0)
|
|
|
|
|
|
def test_yield_perk_and_prestige_boost_harvest_coins(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, perk_yield=4, prestige=1)
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
result = store.harvest(user, 0)
|
|
crop = economy.crop_for("shell")
|
|
assert result["coins"] == economy.realizable_harvest_coins(
|
|
crop, 4, 1, golden=result["golden"]
|
|
)
|
|
assert result["coins"] > crop.reward_coins
|
|
|
|
|
|
def test_xp_perk_boosts_harvest_xp(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, perk_xp=10)
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
result = store.harvest(user, 0)
|
|
crop = economy.crop_for("shell")
|
|
assert result["xp"] == economy.effective_reward_xp(crop, 10)
|
|
assert result["xp"] > crop.reward_xp
|
|
|
|
|
|
# --- water -------------------------------------------------------------------
|
|
|
|
|
|
def test_water_rewards_visitor_and_shortens_build(local_db):
|
|
owner = _reset("unit_a")
|
|
visitor = _reset("unit_b")
|
|
store.plant(owner, 0, "python")
|
|
before_remaining = _remaining(owner)
|
|
before_coins = _coins(visitor)
|
|
result = store.water(visitor, owner, 0)
|
|
assert result["reward_coins"] == economy.WATER_REWARD_COINS
|
|
assert _coins(visitor) == before_coins + economy.WATER_REWARD_COINS
|
|
assert _remaining(owner) < before_remaining
|
|
|
|
|
|
def test_water_own_build_blocked(local_db):
|
|
owner = _reset("unit_a")
|
|
store.plant(owner, 0, "python")
|
|
with pytest.raises(GameError):
|
|
store.water(owner, owner, 0)
|
|
|
|
|
|
def test_water_twice_blocked(local_db):
|
|
owner = _reset("unit_a")
|
|
visitor = _reset("unit_b")
|
|
store.plant(owner, 0, "python")
|
|
store.water(visitor, owner, 0)
|
|
with pytest.raises(GameError):
|
|
store.water(visitor, owner, 0)
|
|
|
|
|
|
def test_water_empty_plot_blocked(local_db):
|
|
owner = _reset("unit_a")
|
|
visitor = _reset("unit_b")
|
|
with pytest.raises(GameError):
|
|
store.water(visitor, owner, 0)
|
|
|
|
|
|
def test_water_maxed_blocked(local_db):
|
|
owner = _reset("unit_a")
|
|
store.plant(owner, 0, "python")
|
|
for index in range(economy.MAX_WATERS_PER_PLOT):
|
|
store.water(_reset(f"unit_w{index}"), owner, 0)
|
|
with pytest.raises(GameError):
|
|
store.water(_reset("unit_w_extra"), owner, 0)
|
|
|
|
|
|
# --- buy plot / upgrade CI ---------------------------------------------------
|
|
|
|
|
|
def test_buy_plot_success(local_db):
|
|
user = _reset("unit_a")
|
|
result = store.buy_plot(user)
|
|
assert result["plot_count"] == economy.STARTING_PLOTS + 1
|
|
assert len(store.get_plots(store.get_farm(user["uid"])["uid"])) == economy.STARTING_PLOTS + 1
|
|
|
|
|
|
def test_buy_plot_at_max_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, plot_count=economy.MAX_PLOTS)
|
|
with pytest.raises(GameError):
|
|
store.buy_plot(user)
|
|
|
|
|
|
def test_buy_plot_insufficient_coins(local_db):
|
|
user = _reset("unit_a", coins=0)
|
|
with pytest.raises(GameError):
|
|
store.buy_plot(user)
|
|
|
|
|
|
def test_upgrade_ci_success(local_db):
|
|
user = _reset("unit_a")
|
|
assert store.upgrade_ci(user)["ci_tier"] == 2
|
|
|
|
|
|
def test_upgrade_ci_at_top_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, ci_tier=economy.MAX_CI_TIER)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_ci(user)
|
|
|
|
|
|
def test_upgrade_ci_insufficient_coins(local_db):
|
|
user = _reset("unit_a", coins=0)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_ci(user)
|
|
|
|
|
|
# --- perks -------------------------------------------------------------------
|
|
|
|
|
|
def test_upgrade_perk_success(local_db):
|
|
user = _reset("unit_a")
|
|
result = store.upgrade_perk(user, "growth")
|
|
assert result["level"] == 1
|
|
assert store.get_farm(user["uid"])["perk_growth"] == 1
|
|
|
|
|
|
def test_upgrade_perk_unknown(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.upgrade_perk(user, "telepathy")
|
|
|
|
|
|
def test_upgrade_perk_maxed(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, perk_growth=economy.perk_for("growth").max_level)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_perk(user, "growth")
|
|
|
|
|
|
def test_upgrade_perk_insufficient_coins(local_db):
|
|
user = _reset("unit_a", coins=0)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_perk(user, "growth")
|
|
|
|
|
|
# --- daily bonus -------------------------------------------------------------
|
|
|
|
|
|
def test_daily_claim_first_time(local_db):
|
|
user = _reset("unit_a")
|
|
result = store.claim_daily(user)
|
|
assert result["streak"] == 1
|
|
assert result["reward"] == economy.daily_reward(1)
|
|
|
|
|
|
def test_daily_double_claim_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
store.claim_daily(user)
|
|
with pytest.raises(GameError):
|
|
store.claim_daily(user)
|
|
|
|
|
|
def test_daily_streak_increments_consecutive_day(local_db):
|
|
user = _reset("unit_a")
|
|
yesterday = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
|
|
_set(user, streak=3, last_daily_at=yesterday)
|
|
assert store.claim_daily(user)["streak"] == 4
|
|
|
|
|
|
def test_daily_streak_resets_after_gap(local_db):
|
|
user = _reset("unit_a")
|
|
two_days_ago = (datetime.now(timezone.utc) - timedelta(days=2)).isoformat()
|
|
_set(user, streak=5, last_daily_at=two_days_ago)
|
|
assert store.claim_daily(user)["streak"] == 1
|
|
|
|
|
|
# --- quests ------------------------------------------------------------------
|
|
|
|
|
|
def _insert_quest(user, kind, goal, progress=0, claimed=0, reward_coins=50, reward_xp=5):
|
|
farm = store.get_farm(user["uid"])
|
|
get_table("game_quests").delete(farm_uid=farm["uid"])
|
|
get_table("game_quests").insert(
|
|
{
|
|
"uid": f"q-{user['uid']}-{kind}",
|
|
"farm_uid": farm["uid"],
|
|
"user_uid": user["uid"],
|
|
"day": store._today(),
|
|
"slot_index": 0,
|
|
"scope": "daily",
|
|
"kind": kind,
|
|
"label": f"{kind} {goal}",
|
|
"goal": goal,
|
|
"progress": progress,
|
|
"reward_coins": reward_coins,
|
|
"reward_xp": reward_xp,
|
|
"claimed": claimed,
|
|
"created_at": store._today(),
|
|
"updated_at": store._today(),
|
|
}
|
|
)
|
|
return farm
|
|
|
|
|
|
def test_ensure_quests_creates_three(local_db):
|
|
user = _reset("unit_a")
|
|
farm = store.get_farm(user["uid"])
|
|
store.ensure_quests(farm, store._today())
|
|
rows = list(get_table("game_quests").find(farm_uid=farm["uid"], day=store._today()))
|
|
assert len(rows) == economy.DAILY_QUEST_COUNT
|
|
|
|
|
|
def test_advance_quests_increments_and_caps(local_db):
|
|
user = _reset("unit_a")
|
|
_insert_quest(user, "plant", goal=3)
|
|
store.advance_quests(user["uid"], "plant", 2)
|
|
farm = store.get_farm(user["uid"])
|
|
row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="plant")
|
|
assert row["progress"] == 2
|
|
store.advance_quests(user["uid"], "plant", 5)
|
|
row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="plant")
|
|
assert row["progress"] == 3
|
|
|
|
|
|
def test_plant_advances_plant_quest(local_db):
|
|
user = _reset("unit_a")
|
|
_insert_quest(user, "plant", goal=5)
|
|
store.plant(user, 0, "shell")
|
|
farm = store.get_farm(user["uid"])
|
|
row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="plant")
|
|
assert row["progress"] == 1
|
|
|
|
|
|
def test_harvest_advances_earn_quest_by_coins(local_db):
|
|
user = _reset("unit_a")
|
|
_insert_quest(user, "earn", goal=1000)
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
result = store.harvest(user, 0)
|
|
farm = store.get_farm(user["uid"])
|
|
row = get_table("game_quests").find_one(farm_uid=farm["uid"], kind="earn")
|
|
assert row["progress"] == result["coins"]
|
|
|
|
|
|
def test_claim_quest_success(local_db):
|
|
user = _reset("unit_a")
|
|
_insert_quest(user, "plant", goal=2, progress=2, reward_coins=70, reward_xp=8)
|
|
before = _coins(user)
|
|
result = store.claim_quest(user, "plant")
|
|
assert result["reward_coins"] == 70
|
|
assert _coins(user) == before + 70
|
|
|
|
|
|
def test_claim_quest_incomplete_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
_insert_quest(user, "plant", goal=5, progress=1)
|
|
with pytest.raises(GameError):
|
|
store.claim_quest(user, "plant")
|
|
|
|
|
|
def test_claim_quest_double_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
_insert_quest(user, "plant", goal=2, progress=2)
|
|
store.claim_quest(user, "plant")
|
|
with pytest.raises(GameError):
|
|
store.claim_quest(user, "plant")
|
|
|
|
|
|
# --- fertilize ---------------------------------------------------------------
|
|
|
|
|
|
def test_fertilize_reduces_time_and_costs_coins(local_db):
|
|
user = _reset("unit_a")
|
|
store.plant(user, 0, "python")
|
|
before_remaining = _remaining(user)
|
|
before_coins = _coins(user)
|
|
result = store.fertilize(user, 0)
|
|
assert _remaining(user) < before_remaining
|
|
assert _coins(user) == before_coins - result["spent"]
|
|
|
|
|
|
def test_fertilize_empty_plot_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.fertilize(user, 0)
|
|
|
|
|
|
def test_fertilize_ready_build_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
with pytest.raises(GameError):
|
|
store.fertilize(user, 0)
|
|
|
|
|
|
# --- prestige ----------------------------------------------------------------
|
|
|
|
|
|
def test_prestige_below_level_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.prestige(user)
|
|
|
|
|
|
def test_prestige_resets_farm_and_increments(local_db):
|
|
user = _reset("unit_a")
|
|
_set(
|
|
user,
|
|
xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL),
|
|
perk_growth=3,
|
|
ci_tier=3,
|
|
)
|
|
store.buy_plot(user)
|
|
coins_before = _coins(user)
|
|
fee = economy.refactor_cost(0, coins_before)
|
|
carried = economy.refactor_carryover(coins_before, fee, 0)
|
|
result = store.prestige(user)
|
|
assert result["prestige"] == 1
|
|
assert result["fee"] == fee
|
|
assert result["carried"] == carried
|
|
farm = store.get_farm(user["uid"])
|
|
assert farm["coins"] == economy.STARTING_COINS + carried
|
|
assert farm["xp"] == 0
|
|
assert farm["ci_tier"] == 1
|
|
assert farm["perk_growth"] == 0
|
|
assert farm["plot_count"] == economy.STARTING_PLOTS
|
|
assert len(store.get_plots(farm["uid"])) == economy.STARTING_PLOTS
|
|
|
|
|
|
def test_prestige_insufficient_coins_blocked(local_db):
|
|
user = _reset("unit_a", coins=100)
|
|
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
|
with pytest.raises(GameError):
|
|
store.prestige(user)
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["prestige"] or 0) == 0
|
|
assert int(farm["coins"]) == 100
|
|
|
|
|
|
def test_prestige_fee_funds_treasury(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
|
fee = economy.refactor_cost(0, _coins(user))
|
|
store.prestige(user)
|
|
assert store.treasury_balance() == fee
|
|
|
|
|
|
def test_prestige_carryover_scales_with_legacy(local_db):
|
|
user = _reset("unit_a")
|
|
_set(
|
|
user,
|
|
xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL),
|
|
legacy_carryover=5,
|
|
)
|
|
coins_before = _coins(user)
|
|
fee = economy.refactor_cost(0, coins_before)
|
|
carried = economy.refactor_carryover(coins_before, fee, 5)
|
|
assert carried > economy.refactor_carryover(coins_before, fee, 0)
|
|
store.prestige(user)
|
|
farm = store.get_farm(user["uid"])
|
|
assert farm["coins"] == economy.STARTING_COINS + carried
|
|
|
|
|
|
def test_prestige_fee_rises_with_prestige_and_wealth(local_db):
|
|
assert economy.refactor_cost(1, 0) > economy.refactor_cost(0, 0)
|
|
assert economy.refactor_cost(0, 1_000_000) > economy.refactor_cost(0, 0)
|
|
|
|
|
|
# --- legacy upgrades ---------------------------------------------------------
|
|
|
|
|
|
def test_upgrade_legacy_success_spends_stars(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, stars=100, legacy_multiplier=0)
|
|
cost = economy.legacy_cost(economy.legacy_for("multiplier"), 0)
|
|
result = store.upgrade_legacy(user, "multiplier")
|
|
assert result["level"] == 1
|
|
assert result["spent"] == cost
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["legacy_multiplier"]) == 1
|
|
assert int(farm["stars"]) == 100 - cost
|
|
|
|
|
|
def test_upgrade_legacy_unknown_key(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, stars=100)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_legacy(user, "telekinesis")
|
|
|
|
|
|
def test_upgrade_legacy_maxed(local_db):
|
|
user = _reset("unit_a")
|
|
up = economy.legacy_for("multiplier")
|
|
_set(user, stars=100000, legacy_multiplier=up.max_level)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_legacy(user, "multiplier")
|
|
|
|
|
|
def test_upgrade_legacy_insufficient_stars(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, stars=0, legacy_multiplier=0)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_legacy(user, "multiplier")
|
|
|
|
|
|
# --- stealing ----------------------------------------------------------------
|
|
|
|
|
|
def _ripen_past_grace(user, slot=0):
|
|
farm = store.get_farm(user["uid"])
|
|
past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
|
|
plot = store._plot_at(farm["uid"], slot)
|
|
get_table("game_plots").update(
|
|
{"uid": plot["uid"], "planted_at": past, "ready_at": past}, ["uid"]
|
|
)
|
|
|
|
|
|
def test_steal_transfers_a_share_and_leaves_the_build(local_db):
|
|
owner = _reset("unit_owner")
|
|
thief = _reset("unit_thief", coins=0)
|
|
store.plant(owner, 0, "shell")
|
|
_ripen_past_grace(owner)
|
|
result = store.steal(thief, owner, 0)
|
|
assert result["coins"] > 0
|
|
assert _coins(thief) == result["coins"]
|
|
farm = store.get_farm(owner["uid"])
|
|
plot = store._plot_at(farm["uid"], 0)
|
|
assert (plot.get("crop_key") or "") == "shell"
|
|
assert 0 < float(plot.get("raided_fraction") or 0) <= 1.0
|
|
|
|
|
|
def test_owner_still_harvests_the_unraided_remainder(local_db):
|
|
owner = _reset("unit_owner")
|
|
thief = _reset("unit_thief", coins=0)
|
|
store.plant(owner, 0, "shell")
|
|
_ripen_past_grace(owner)
|
|
before = _coins(owner)
|
|
stolen = store.steal(thief, owner, 0)["coins"]
|
|
result = store.harvest(owner, 0)
|
|
harvested = result["coins"]
|
|
assert harvested > 0
|
|
assert _coins(owner) == before + harvested
|
|
full = economy.realizable_harvest_coins(
|
|
economy.crop_for("shell"), golden=result["golden"]
|
|
)
|
|
assert harvested + stolen <= full
|
|
|
|
|
|
def test_steal_blocked_once_the_build_is_fully_stripped(local_db):
|
|
owner = _reset("unit_owner")
|
|
store.plant(owner, 0, "shell")
|
|
_ripen_past_grace(owner)
|
|
farm = store.get_farm(owner["uid"])
|
|
plot = store._plot_at(farm["uid"], 0)
|
|
get_table("game_plots").update(
|
|
{"uid": plot["uid"], "raided_fraction": 1.0}, ["uid"]
|
|
)
|
|
thief = _reset("unit_thief", coins=0)
|
|
with pytest.raises(GameError):
|
|
store.steal(thief, owner, 0)
|
|
|
|
|
|
def test_steal_capped_per_victim_per_day(local_db):
|
|
owner = _reset("unit_owner")
|
|
store.plant(owner, 0, "shell")
|
|
_ripen_past_grace(owner)
|
|
for index in range(economy.STEAL_MAX_PER_VICTIM_PER_DAY):
|
|
thief = _reset(f"unit_thief_{index}", coins=0)
|
|
store.steal(thief, owner, 0)
|
|
farm = store.get_farm(owner["uid"])
|
|
plot = store._plot_at(farm["uid"], 0)
|
|
get_table("game_plots").update(
|
|
{"uid": plot["uid"], "raided_fraction": 0.0}, ["uid"]
|
|
)
|
|
blocked = _reset("unit_thief_last", coins=0)
|
|
with pytest.raises(GameError):
|
|
store.steal(blocked, owner, 0)
|
|
|
|
|
|
def test_steal_protected_within_grace(local_db):
|
|
owner = _reset("unit_owner")
|
|
thief = _reset("unit_thief")
|
|
store.plant(owner, 0, "shell")
|
|
farm = store.get_farm(owner["uid"])
|
|
past = (datetime.now(timezone.utc) - timedelta(seconds=5)).isoformat()
|
|
plot = store._plot_at(farm["uid"], 0)
|
|
get_table("game_plots").update(
|
|
{"uid": plot["uid"], "planted_at": past, "ready_at": past}, ["uid"]
|
|
)
|
|
with pytest.raises(GameError):
|
|
store.steal(thief, owner, 0)
|
|
|
|
|
|
def test_steal_cooldown_blocks_second_raid(local_db):
|
|
owner = _reset("unit_owner")
|
|
thief = _reset("unit_thief", coins=0)
|
|
store.plant(owner, 0, "shell")
|
|
_ripen_past_grace(owner)
|
|
store.steal(thief, owner, 0)
|
|
store.harvest(owner, 0)
|
|
store.plant(owner, 0, "shell")
|
|
_ripen_past_grace(owner)
|
|
with pytest.raises(GameError):
|
|
store.steal(thief, owner, 0)
|
|
|
|
|
|
# --- backwards compatibility -------------------------------------------------
|
|
|
|
|
|
def test_serialize_handles_legacy_row_without_new_columns(local_db):
|
|
user = _user("unit_legacy")
|
|
existing = store.get_farm(user["uid"])
|
|
if existing:
|
|
get_table("game_plots").delete(farm_uid=existing["uid"])
|
|
get_table("game_quests").delete(farm_uid=existing["uid"])
|
|
get_table("game_farms").delete(uid=existing["uid"])
|
|
now = datetime.now(timezone.utc).isoformat()
|
|
get_table("game_farms").insert(
|
|
{
|
|
"uid": "legacy-farm",
|
|
"user_uid": user["uid"],
|
|
"coins": 50,
|
|
"xp": 0,
|
|
"level": 1,
|
|
"ci_tier": 1,
|
|
"plot_count": economy.STARTING_PLOTS,
|
|
"total_harvests": 0,
|
|
"created_at": now,
|
|
"updated_at": now,
|
|
}
|
|
)
|
|
farm = get_table("game_farms").find_one(uid="legacy-farm")
|
|
data = store.serialize_farm(farm, viewer=user, owner=user)
|
|
assert data["prestige"] == 0
|
|
assert data["streak"] == 0
|
|
assert data["daily_available"] is True
|
|
assert all(perk["level"] == 0 for perk in data["perks"])
|
|
|
|
|
|
# --- leaderboard -------------------------------------------------------------
|
|
|
|
|
|
def test_leaderboard_orders_by_score(local_db):
|
|
high = _reset("unit_lb_high")
|
|
low = _reset("unit_lb_low")
|
|
_set(high, level=8, xp=economy.xp_threshold(8))
|
|
_set(low, level=1, xp=0)
|
|
board = store.leaderboard(50)
|
|
names = [entry["username"] for entry in board]
|
|
assert "unit_lb_high" in names and "unit_lb_low" in names
|
|
assert names.index("unit_lb_high") < names.index("unit_lb_low")
|
|
|
|
|
|
def test_leaderboard_prestige_lifts_above_lower_level(local_db):
|
|
refactorer = _reset("unit_lb_refactor")
|
|
grinder = _reset("unit_lb_grind")
|
|
_set(refactorer, level=1, xp=200, prestige=1)
|
|
_set(grinder, level=5, xp=economy.xp_threshold(5))
|
|
board = store.leaderboard(50)
|
|
names = [entry["username"] for entry in board]
|
|
assert names.index("unit_lb_refactor") < names.index("unit_lb_grind")
|
|
|
|
|
|
def test_leaderboard_entry_exposes_score_and_prestige(local_db):
|
|
user = _reset("unit_lb_fields")
|
|
_set(user, prestige=3, total_harvests=12, ci_tier=2)
|
|
entry = next(e for e in store.leaderboard(50) if e["username"] == "unit_lb_fields")
|
|
farm = store.get_farm(user["uid"])
|
|
assert entry["prestige"] == 3
|
|
assert entry["score"] == economy.farm_score(farm)
|
|
assert {"rank", "username", "level", "xp", "coins", "total_harvests", "prestige", "score"} <= set(entry)
|
|
|
|
|
|
# --- market saturation ---------------------------------------------------------
|
|
|
|
|
|
def test_record_and_recent_harvests_roundtrip(local_db):
|
|
_reset("unit_a")
|
|
from devplacepy.services.game.store import market as market_store
|
|
|
|
now = datetime.now(timezone.utc)
|
|
market_store.record_harvest_tick("shell", now)
|
|
market_store.record_harvest_tick("shell", now)
|
|
assert store.recent_harvests("shell", economy.MARKET_WINDOW_HOURS) == 2
|
|
assert store.recent_harvests("python", economy.MARKET_WINDOW_HOURS) == 0
|
|
|
|
|
|
def test_harvest_records_market_tick_and_saturates_reward(local_db):
|
|
user = _reset("unit_a")
|
|
from devplacepy.services.game.store import market as market_store
|
|
from devplacepy.utils import generate_uid
|
|
|
|
now = datetime.now(timezone.utc)
|
|
crop = economy.crop_for("shell")
|
|
get_table("game_market_ticks").insert(
|
|
{
|
|
"uid": generate_uid(),
|
|
"crop_key": "shell",
|
|
"hour_bucket": market_store._hour_bucket(now),
|
|
"harvests": _worst_tier_harvests("shell"),
|
|
"updated_at": now.isoformat(),
|
|
}
|
|
)
|
|
market_store._saturation_cache.clear()
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
result = store.harvest(user, 0)
|
|
unsaturated = economy.realizable_harvest_coins(crop, golden=result["golden"])
|
|
assert result["coins"] < unsaturated
|
|
|
|
|
|
def _seed_market_ticks(crop_key, harvests):
|
|
from devplacepy.services.game.store import market as market_store
|
|
from devplacepy.utils import generate_uid
|
|
|
|
now = datetime.now(timezone.utc)
|
|
get_table("game_market_ticks").insert(
|
|
{
|
|
"uid": generate_uid(),
|
|
"crop_key": crop_key,
|
|
"hour_bucket": market_store._hour_bucket(now),
|
|
"harvests": harvests,
|
|
"updated_at": now.isoformat(),
|
|
}
|
|
)
|
|
market_store._saturation_cache.clear()
|
|
|
|
|
|
def _worst_tier_harvests(crop_key):
|
|
from devplacepy.services.game.store import market as market_store
|
|
|
|
crop = economy.crop_for(crop_key)
|
|
worst_days = economy.MARKET_SATURATION_TIERS[-1][0]
|
|
supply_per_harvest = crop.grow_seconds / 86400.0 / market_store.active_farms()
|
|
return int(worst_days / supply_per_harvest) + 1
|
|
|
|
|
|
def test_starter_crop_boosted_when_market_saturated(local_db):
|
|
_reset("unit_a")
|
|
from devplacepy.services.game.store import market as market_store
|
|
|
|
_seed_market_ticks("kernel", _worst_tier_harvests("kernel"))
|
|
assert market_store.market_factor_for("kernel") == economy.MARKET_SATURATION_TIERS[-1][1]
|
|
assert market_store.market_factor_for("shell") == economy.MARKET_BUFF_CAP
|
|
assert market_store.market_factor_for("rust") == 1.0
|
|
|
|
|
|
def test_own_saturation_beats_boost(local_db):
|
|
_reset("unit_a")
|
|
from devplacepy.services.game.store import market as market_store
|
|
|
|
_seed_market_ticks("kernel", _worst_tier_harvests("kernel"))
|
|
_seed_market_ticks("shell", _worst_tier_harvests("shell"))
|
|
assert market_store.market_factor_for("shell") == economy.MARKET_SATURATION_TIERS[-1][1]
|
|
|
|
|
|
def test_harvest_records_a_market_tick_for_its_own_crop(local_db):
|
|
user = _reset("unit_a")
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
store.harvest(user, 0)
|
|
assert store.recent_harvests("shell", economy.MARKET_WINDOW_HOURS) == 1
|
|
|
|
|
|
# --- infrastructure --------------------------------------------------------------
|
|
|
|
|
|
def test_buy_infrastructure_success(local_db):
|
|
cost = economy.infra_for("registry").cost
|
|
user = _reset("unit_a", coins=cost + 100000)
|
|
_set(user, prestige=5)
|
|
result = store.buy_infrastructure(user, "registry")
|
|
assert result["key"] == "registry"
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["infra_registry"]) == 1
|
|
assert _coins(user) == 100000
|
|
|
|
|
|
def test_buy_infrastructure_requires_prestige(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.buy_infrastructure(user, "registry")
|
|
|
|
|
|
def test_buy_infrastructure_already_owned_blocked(local_db):
|
|
cost = economy.infra_for("registry").cost
|
|
user = _reset("unit_a", coins=cost * 2)
|
|
_set(user, prestige=5)
|
|
store.buy_infrastructure(user, "registry")
|
|
with pytest.raises(GameError):
|
|
store.buy_infrastructure(user, "registry")
|
|
|
|
|
|
def test_buy_infrastructure_unknown_key(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.buy_infrastructure(user, "does_not_exist")
|
|
|
|
|
|
# --- defense -----------------------------------------------------------------------
|
|
|
|
|
|
def test_upgrade_defense_success(local_db):
|
|
user = _reset("unit_a")
|
|
result = store.upgrade_defense(user)
|
|
assert result["defense_level"] == 1
|
|
assert int(store.get_farm(user["uid"])["defense_level"]) == 1
|
|
|
|
|
|
def test_upgrade_defense_insufficient_coins(local_db):
|
|
user = _reset("unit_a", coins=0)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_defense(user)
|
|
|
|
|
|
def test_charge_upkeep_decays_when_unaffordable(local_db):
|
|
user = _reset("unit_a", coins=0)
|
|
_set(user, defense_level=1, defense_last_upkeep_at=(datetime.now(timezone.utc) - timedelta(days=3)).isoformat())
|
|
farm = store.get_farm(user["uid"])
|
|
updated = store.charge_upkeep(farm, datetime.now(timezone.utc))
|
|
assert int(updated["defense_level"]) == 0
|
|
assert int(updated["coins"]) == 0
|
|
|
|
|
|
def test_charge_upkeep_deducts_coins_when_affordable(local_db):
|
|
user = _reset("unit_a", coins=100000)
|
|
_set(user, defense_level=1, defense_last_upkeep_at=(datetime.now(timezone.utc) - timedelta(days=1)).isoformat())
|
|
farm = store.get_farm(user["uid"])
|
|
updated = store.charge_upkeep(farm, datetime.now(timezone.utc))
|
|
assert int(updated["defense_level"]) == 1
|
|
assert int(updated["coins"]) < 100000
|
|
|
|
|
|
# --- cosmetics ---------------------------------------------------------------------
|
|
|
|
|
|
def test_buy_and_equip_cosmetic(local_db):
|
|
user = _reset("unit_a", coins=economy.cosmetic_for("title_architect").cost_coins * 2)
|
|
store.buy_cosmetic(user, "title_architect")
|
|
assert "title_architect" in store.owned_cosmetic_keys(user["uid"])
|
|
result = store.equip_title(user, "title_architect")
|
|
assert result["active_title"] == "title_architect"
|
|
assert store.get_farm(user["uid"])["active_title"] == "title_architect"
|
|
|
|
|
|
def test_buy_cosmetic_twice_blocked(local_db):
|
|
user = _reset("unit_a", coins=economy.cosmetic_for("title_architect").cost_coins * 2)
|
|
store.buy_cosmetic(user, "title_architect")
|
|
with pytest.raises(GameError):
|
|
store.buy_cosmetic(user, "title_architect")
|
|
|
|
|
|
def test_equip_unowned_cosmetic_blocked(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.equip_title(user, "title_architect")
|
|
|
|
|
|
def test_equip_non_title_cosmetic_blocked(local_db):
|
|
user = _reset("unit_a", coins=economy.cosmetic_for("skin_neon").cost_coins * 2)
|
|
store.buy_cosmetic(user, "skin_neon")
|
|
with pytest.raises(GameError):
|
|
store.equip_title(user, "skin_neon")
|
|
|
|
|
|
# --- mastery -------------------------------------------------------------------------
|
|
|
|
|
|
def test_upgrade_mastery_success(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, mastery_points=10)
|
|
result = store.upgrade_mastery(user, "autoreplant")
|
|
assert result["level"] == 1
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["mastery_autoreplant"]) == 1
|
|
assert int(farm["mastery_points"]) == 10 - economy.mastery_cost(economy.mastery_for("autoreplant"), 0)
|
|
|
|
|
|
def test_upgrade_mastery_insufficient_points(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.upgrade_mastery(user, "autoreplant")
|
|
|
|
|
|
def test_upgrade_mastery_unknown_key(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, mastery_points=1000)
|
|
with pytest.raises(GameError):
|
|
store.upgrade_mastery(user, "does_not_exist")
|
|
|
|
|
|
def test_farm_analytics_hidden_until_unlocked(local_db):
|
|
user = _reset("unit_a")
|
|
state = store.serialize_farm(store.get_farm(user["uid"]), viewer=user, owner=user)
|
|
assert state["mastery_analytics_unlocked"] is False
|
|
|
|
|
|
def test_farm_analytics_tracks_lifetime_totals_once_unlocked(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, mastery_points=1000)
|
|
store.upgrade_mastery(user, "analytics")
|
|
store.plant(user, 0, "shell")
|
|
_warp(user)
|
|
result = store.harvest(user, 0)
|
|
state = store.serialize_farm(store.get_farm(user["uid"]), viewer=user, owner=user)
|
|
assert state["mastery_analytics_unlocked"] is True
|
|
assert state["lifetime_coins_earned"] == result["coins"]
|
|
assert state["lifetime_harvests"] == 1
|
|
|
|
|
|
def test_prestige_awards_mastery_at_threshold(local_db):
|
|
user = _reset("unit_a")
|
|
coins = economy.refactor_cost(49, 0) * 2
|
|
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=49, coins=coins)
|
|
result = store.prestige(user)
|
|
assert result["mastery_awarded"] == 1
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["mastery_points"]) == 1
|
|
assert int(farm["mastery_points_earned_total"]) == 1
|
|
|
|
|
|
def test_prestige_below_mastery_threshold_awards_nothing(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=0)
|
|
result = store.prestige(user)
|
|
assert result["mastery_awarded"] == 0
|
|
|
|
|
|
# --- eras --------------------------------------------------------------------------
|
|
|
|
|
|
def test_start_era_resets_visible_counters_not_real_balances(local_db):
|
|
user = _reset("unit_a", coins=12345)
|
|
_set(user, era_coins=999, era_harvests=5, prestige=3, stars=7)
|
|
if store.active_era():
|
|
store.end_era()
|
|
store.start_era("TestEra", 7)
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["era_coins"]) == 0
|
|
assert int(farm["era_harvests"]) == 0
|
|
assert int(farm["coins"]) == 12345
|
|
assert int(farm["prestige"]) == 3
|
|
assert int(farm["stars"]) == 7
|
|
store.end_era()
|
|
|
|
|
|
def test_start_era_twice_blocked(local_db):
|
|
if store.active_era():
|
|
store.end_era()
|
|
store.start_era("TestEra", 7)
|
|
with pytest.raises(GameError):
|
|
store.start_era("AnotherEra", 7)
|
|
store.end_era()
|
|
|
|
|
|
def test_end_era_without_active_blocked(local_db):
|
|
if store.active_era():
|
|
store.end_era()
|
|
with pytest.raises(GameError):
|
|
store.end_era()
|
|
|
|
|
|
def test_end_era_awards_stars_to_top_participant(local_db):
|
|
if store.active_era():
|
|
store.end_era()
|
|
user = _reset("unit_era_top")
|
|
store.start_era("TestEra", 7)
|
|
_set(user, era_coins=50000, era_harvests=10)
|
|
before_stars = int(store.get_farm(user["uid"])["stars"] or 0)
|
|
store.end_era()
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["stars"] or 0) > before_stars
|
|
assert int(farm["era_coins"] or 0) == 0
|
|
|
|
|
|
def test_era_gates_new_crops_and_leaderboard(local_db):
|
|
if store.active_era():
|
|
store.end_era()
|
|
_clear_game_caches()
|
|
assert store.leaderboard_for("era") == []
|
|
|
|
|
|
# --- weekly contracts ------------------------------------------------------------------
|
|
|
|
|
|
def test_weekly_contract_requires_mastery_upgrade(local_db):
|
|
user = _reset("unit_a")
|
|
with pytest.raises(GameError):
|
|
store.claim_quest(user, "harvest", "weekly")
|
|
|
|
|
|
def test_weekly_contract_claim_pays_stars_and_boost(local_db):
|
|
user = _reset("unit_a")
|
|
_set(user, mastery_points=1000)
|
|
store.upgrade_mastery(user, "contracts")
|
|
farm = store.get_farm(user["uid"])
|
|
from devplacepy.services.game.store.quests import ensure_weekly_contract
|
|
from devplacepy.services.game.store.common import _iso_week
|
|
|
|
iso_week = _iso_week(datetime.now(timezone.utc))
|
|
ensure_weekly_contract(farm, iso_week)
|
|
row = get_table("game_quests").find_one(farm_uid=farm["uid"], day=iso_week, scope="weekly")
|
|
get_table("game_quests").update({"uid": row["uid"], "progress": row["goal"]}, ["uid"])
|
|
result = store.claim_quest(user, row["kind"], "weekly")
|
|
assert result["reward_stars"] > 0
|
|
farm = store.get_farm(user["uid"])
|
|
assert int(farm["stars"]) == result["reward_stars"]
|
|
assert farm.get("contract_boost_until")
|
|
|
|
|
|
# --- treasury and community grant --------------------------------------------
|
|
|
|
|
|
def _seed_treasury(amount):
|
|
store.ensure_treasury()
|
|
get_table("game_treasury").update(
|
|
{"uid": "treasury-main", "balance": amount}, ["uid"]
|
|
)
|
|
|
|
|
|
def _make_grant_eligible(user):
|
|
_set(
|
|
user,
|
|
coins=100,
|
|
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
|
harvests_week_start=store._iso_week(store._now()),
|
|
)
|
|
|
|
|
|
def test_claim_grant_pays_from_treasury(local_db):
|
|
user = _reset("unit_a")
|
|
_seed_treasury(100_000)
|
|
_make_grant_eligible(user)
|
|
result = store.claim_grant(user)
|
|
assert result["amount"] == economy.GRANT_CAP
|
|
assert _coins(user) == 100 + economy.GRANT_CAP
|
|
assert store.treasury_balance() == 100_000 - economy.GRANT_CAP
|
|
|
|
|
|
def test_claim_grant_once_per_week(local_db):
|
|
user = _reset("unit_a")
|
|
_seed_treasury(100_000)
|
|
_make_grant_eligible(user)
|
|
store.claim_grant(user)
|
|
_set(user, coins=100)
|
|
with pytest.raises(GameError):
|
|
store.claim_grant(user)
|
|
|
|
|
|
def test_claim_grant_requires_activity(local_db):
|
|
user = _reset("unit_a")
|
|
_seed_treasury(100_000)
|
|
_set(user, coins=100, harvests_week=economy.GRANT_MIN_WEEK_HARVESTS - 1)
|
|
with pytest.raises(GameError):
|
|
store.claim_grant(user)
|
|
|
|
|
|
def test_claim_grant_wealth_ceiling(local_db):
|
|
user = _reset("unit_a")
|
|
_seed_treasury(100_000)
|
|
_set(
|
|
user,
|
|
coins=economy.GRANT_WEALTH_CEILING,
|
|
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
|
)
|
|
with pytest.raises(GameError):
|
|
store.claim_grant(user)
|
|
|
|
|
|
def test_claim_grant_prestige_cap(local_db):
|
|
user = _reset("unit_a")
|
|
_seed_treasury(100_000)
|
|
_make_grant_eligible(user)
|
|
_set(user, prestige=economy.GRANT_MAX_PRESTIGE + 1)
|
|
with pytest.raises(GameError):
|
|
store.claim_grant(user)
|
|
|
|
|
|
def test_claim_grant_empty_treasury(local_db):
|
|
user = _reset("unit_a")
|
|
_make_grant_eligible(user)
|
|
with pytest.raises(GameError):
|
|
store.claim_grant(user)
|
|
|
|
|
|
def test_claim_grant_partial_treasury(local_db):
|
|
user = _reset("unit_a")
|
|
_seed_treasury(300)
|
|
_make_grant_eligible(user)
|
|
result = store.claim_grant(user)
|
|
assert result["amount"] == 300
|
|
assert store.treasury_balance() == 0
|