feat: add Code Farm cooperative idle game with plot-based crop system and pub/sub notifications
Implement a Farmville-style cooperative idle game mounted at `/game` with member-only play and public farm viewing. The data layer is pure and timestamp-driven with no background tick: plot states are derived from `ready_at` vs current time, never stored. Key features include plantable software projects (shell script through kernel) that build over real time, harvest for coins and XP, CI tier upgrades for faster builds, plot purchasing with doubling cost, watering mechanics for friends' builds, daily quests, and prestige system. All game endpoints negotiate HTML or JSON and return full farm state for single-request client refresh. Pub/sub notifications broadcast farm updates on `public.game.farm.{username}` topics. Database schema adds `game_farms`, `game_plots`, and `game_quests` tables with appropriate indexes.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from devplacepy.services.game import economy
|
||||
|
||||
|
||||
def test_level_thresholds():
|
||||
assert economy.level_for_xp(0) == 1
|
||||
assert economy.level_for_xp(economy.xp_threshold(2)) == 2
|
||||
assert economy.level_for_xp(economy.xp_threshold(5)) == 5
|
||||
assert economy.level_for_xp(economy.xp_threshold(economy.MAX_LEVEL) + 9999) == economy.MAX_LEVEL
|
||||
|
||||
|
||||
def test_level_progress_span():
|
||||
progress = economy.level_progress(economy.xp_threshold(3))
|
||||
assert progress["level"] == 3
|
||||
assert progress["into_level"] == 0
|
||||
assert progress["is_max"] is False
|
||||
|
||||
|
||||
def test_plot_cost_doubles():
|
||||
assert economy.plot_cost(economy.STARTING_PLOTS) == economy.PLOT_BASE_COST
|
||||
assert economy.plot_cost(economy.STARTING_PLOTS + 1) == economy.PLOT_BASE_COST * 2
|
||||
assert economy.plot_cost(economy.STARTING_PLOTS + 2) == economy.PLOT_BASE_COST * 4
|
||||
|
||||
|
||||
def test_perk_cost_grows():
|
||||
perk = economy.perk_for("yield")
|
||||
assert economy.perk_cost(perk, 0) == perk.base_cost
|
||||
assert economy.perk_cost(perk, 1) == round(perk.base_cost * perk.cost_growth)
|
||||
assert economy.perk_cost(perk, 2) > economy.perk_cost(perk, 1)
|
||||
|
||||
|
||||
def test_daily_reward_streak_and_cap():
|
||||
assert economy.daily_reward(1) == economy.DAILY_BASE
|
||||
assert economy.daily_reward(2) == economy.DAILY_BASE + economy.DAILY_STREAK_STEP
|
||||
assert economy.daily_reward(99) == economy.daily_reward(economy.DAILY_STREAK_CAP)
|
||||
|
||||
|
||||
def test_effective_plant_cost_discount():
|
||||
crop = economy.crop_for("python")
|
||||
assert economy.effective_plant_cost(crop, 0) == crop.cost
|
||||
assert economy.effective_plant_cost(crop, 3) < crop.cost
|
||||
assert economy.effective_plant_cost(crop, 3) >= 1
|
||||
|
||||
|
||||
def test_effective_reward_coins_yield_and_prestige():
|
||||
crop = economy.crop_for("shell")
|
||||
assert economy.effective_reward_coins(crop, 0, 0) == crop.reward_coins
|
||||
assert economy.effective_reward_coins(crop, 4, 0) > crop.reward_coins
|
||||
assert economy.effective_reward_coins(crop, 0, 2) > crop.reward_coins
|
||||
|
||||
|
||||
def test_effective_reward_xp():
|
||||
crop = economy.crop_for("shell")
|
||||
assert economy.effective_reward_xp(crop, 0) == crop.reward_xp
|
||||
assert economy.effective_reward_xp(crop, 10) > crop.reward_xp
|
||||
|
||||
|
||||
def test_growth_perk_speeds_build():
|
||||
crop = economy.crop_for("python")
|
||||
assert economy.grow_seconds_for(crop, 1, 5) < economy.grow_seconds_for(crop, 1, 0)
|
||||
|
||||
|
||||
def test_ci_tier_speeds_build():
|
||||
crop = economy.crop_for("python")
|
||||
assert economy.grow_seconds_for(crop, 2, 0) < economy.grow_seconds_for(crop, 1, 0)
|
||||
|
||||
|
||||
def test_prestige_multiplier():
|
||||
assert economy.prestige_multiplier(0) == 1.0
|
||||
assert economy.prestige_multiplier(2) == 1 + 2 * economy.PRESTIGE_BONUS
|
||||
|
||||
|
||||
def test_fertilize_cost_has_floor():
|
||||
assert economy.fertilize_cost(1) == economy.FERTILIZE_MIN_COST
|
||||
assert economy.fertilize_cost(10000) > economy.FERTILIZE_MIN_COST
|
||||
|
||||
|
||||
def test_crop_payload_locked_flag():
|
||||
rust = economy.crop_for("rust")
|
||||
assert economy.crop_payload(rust, 1, 1)["locked"] is True
|
||||
assert economy.crop_payload(rust, 1, rust.min_level)["locked"] is False
|
||||
|
||||
|
||||
def test_crop_payload_reflects_perks():
|
||||
crop = economy.crop_for("python")
|
||||
base = economy.crop_payload(crop, 1, 5)
|
||||
boosted = economy.crop_payload(crop, 1, 5, growth_level=5, discount_level=3, yield_level=4, prestige=1)
|
||||
assert boosted["cost"] < base["cost"]
|
||||
assert boosted["grow_seconds"] < base["grow_seconds"]
|
||||
assert boosted["reward_coins"] > base["reward_coins"]
|
||||
|
||||
|
||||
def test_daily_quests_deterministic_count_and_kinds():
|
||||
first = economy.daily_quests("user-xyz", "2026-06-22")
|
||||
second = economy.daily_quests("user-xyz", "2026-06-22")
|
||||
assert first == second
|
||||
assert len(first) == economy.DAILY_QUEST_COUNT
|
||||
assert all(quest["kind"] in economy.QUEST_DEFS for quest in first)
|
||||
assert all(quest["goal"] > 0 for quest in first)
|
||||
@@ -0,0 +1,537 @@
|
||||
# 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 _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"])
|
||||
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)
|
||||
result = store.prestige(user)
|
||||
assert result["prestige"] == 1
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert farm["coins"] == economy.STARTING_COINS
|
||||
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
|
||||
|
||||
|
||||
# --- 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_level_then_xp(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")
|
||||
Reference in New Issue
Block a user