1079 lines
34 KiB
Python
Raw Normal View History

# 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,
"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()
2026-07-23 01:14:10 +02:00
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")
assert result["coins"] == crop.reward_coins
assert result["xp"] == crop.reward_xp
assert _coins(user) == before + crop.reward_coins
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.effective_reward_coins(crop, 4, 1)
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,
"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)
2026-07-23 01:14:10 +02:00
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
2026-07-23 01:14:10 +02:00
assert result["fee"] == fee
assert result["carried"] == carried
farm = store.get_farm(user["uid"])
2026-07-23 01:14:10 +02:00
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
2026-07-23 01:14:10 +02:00
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_coins_and_clears_plot(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"])
assert (store._plot_at(farm["uid"], 0).get("crop_key") or "") == ""
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.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)
worst_tier_count = economy.MARKET_SATURATION_TIERS[-1][0]
get_table("game_market_ticks").insert(
{
"uid": generate_uid(),
"crop_key": "shell",
"hour_bucket": market_store._hour_bucket(now),
"harvests": worst_tier_count,
"updated_at": now.isoformat(),
}
)
store.plant(user, 0, "shell")
_warp(user)
crop = economy.crop_for("shell")
result = store.harvest(user, 0)
assert result["coins"] < crop.reward_coins
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")
2026-07-23 01:14:10 +02:00
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"])
store.end_era()
farm = store.get_farm(user["uid"])
assert int(farm["stars"]) > before_stars
assert int(farm["era_coins"]) == 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")
2026-07-23 01:14:10 +02:00
# --- 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