forked from retoor/devplacepy
Code Farm economy rebalance: market saturation, infrastructure/defense/cosmetics, mastery track, secondary leaderboards, admin eras, underdog bonus and weekly contracts
Every purchase/upgrade path (new and pre-existing) is now race-safe against concurrent requests via atomic conditional SQL updates. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,15 @@ def _user(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"])
|
||||
@@ -34,6 +43,9 @@ def _reset(username, coins=100000):
|
||||
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()
|
||||
_clear_game_caches()
|
||||
farm = store.ensure_farm(user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
||||
return user
|
||||
@@ -644,3 +656,293 @@ def test_leaderboard_entry_exposes_score_and_prestige(local_db):
|
||||
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")
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=49)
|
||||
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")
|
||||
|
||||
Reference in New Issue
Block a user