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:
@@ -36,6 +36,7 @@ def _reset_farm(username, coins=100000):
|
||||
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_market_ticks").delete()
|
||||
farm = store.ensure_farm(user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
||||
return user
|
||||
|
||||
@@ -37,6 +37,7 @@ def _reset_farm(username, coins=100000):
|
||||
if farm:
|
||||
get_table("game_plots").delete(farm_uid=farm["uid"])
|
||||
get_table("game_farms").delete(uid=farm["uid"])
|
||||
get_table("game_market_ticks").delete()
|
||||
farm = store.ensure_farm(user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
||||
refresh_snapshot()
|
||||
@@ -189,6 +190,130 @@ def _ripen_owner_plot(owner, slot=0):
|
||||
return plot
|
||||
|
||||
|
||||
def _set_farm(username, **fields):
|
||||
refresh_snapshot()
|
||||
user = get_table("users").find_one(username=username)
|
||||
farm = store.get_farm(user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], **fields}, ["uid"])
|
||||
refresh_snapshot()
|
||||
|
||||
|
||||
def test_defense_upgrade_requires_auth(app_server, seeded_db):
|
||||
r = requests.post(f"{BASE_URL}/game/defense/upgrade", headers=JSON)
|
||||
assert r.status_code in (401, 303)
|
||||
|
||||
|
||||
def test_defense_upgrade_success(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
r = session.post(f"{BASE_URL}/game/defense/upgrade", headers=JSON)
|
||||
assert r.status_code == 200, r.text[:200]
|
||||
assert r.json()["farm"]["defense_level"] == 1
|
||||
|
||||
|
||||
def test_defense_upgrade_insufficient_coins_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=0)
|
||||
r = session.post(f"{BASE_URL}/game/defense/upgrade", headers=JSON)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_infrastructure_buy_requires_prestige(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100_000_000)
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/infrastructure/buy", data={"key": "registry"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_infrastructure_buy_success(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100_000_000)
|
||||
_set_farm(name, prestige=5)
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/infrastructure/buy", data={"key": "registry"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 200, r.text[:200]
|
||||
farm = r.json()["farm"]
|
||||
owned = next(i for i in farm["infrastructure"] if i["key"] == "registry")
|
||||
assert owned["owned"] is True
|
||||
|
||||
|
||||
def test_infrastructure_buy_unknown_key_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/infrastructure/buy", data={"key": "does_not_exist"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_cosmetics_buy_requires_auth(app_server, seeded_db):
|
||||
r = requests.post(
|
||||
f"{BASE_URL}/game/cosmetics/buy", data={"key": "title_architect"}, headers=JSON
|
||||
)
|
||||
assert r.status_code in (401, 303)
|
||||
|
||||
|
||||
def test_cosmetics_buy_and_equip(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=1_000_000)
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/cosmetics/buy", data={"key": "title_architect"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 200, r.text[:200]
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/cosmetics/equip", data={"key": "title_architect"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 200, r.text[:200]
|
||||
assert r.json()["farm"]["active_title"] == "title_architect"
|
||||
|
||||
|
||||
def test_cosmetics_buy_insufficient_coins_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=0)
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/cosmetics/buy", data={"key": "title_architect"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_mastery_upgrade_requires_auth(app_server, seeded_db):
|
||||
r = requests.post(
|
||||
f"{BASE_URL}/game/mastery", data={"key": "autoreplant"}, headers=JSON
|
||||
)
|
||||
assert r.status_code in (401, 303)
|
||||
|
||||
|
||||
def test_mastery_upgrade_insufficient_points_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/mastery", data={"key": "autoreplant"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 400
|
||||
|
||||
|
||||
def test_mastery_upgrade_success(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
_set_farm(name, mastery_points=1000)
|
||||
r = session.post(
|
||||
f"{BASE_URL}/game/mastery", data={"key": "autoreplant"}, headers=JSON
|
||||
)
|
||||
assert r.status_code == 200, r.text[:200]
|
||||
upgrade = next(m for m in r.json()["farm"]["mastery"] if m["key"] == "autoreplant")
|
||||
assert upgrade["level"] == 1
|
||||
|
||||
|
||||
def test_leaderboard_boards_all_return_200(app_server, seeded_db):
|
||||
for board in ("score", "prestige", "harvests", "raids", "time_to_kernel", "fair_play", "era"):
|
||||
r = requests.get(f"{BASE_URL}/game/leaderboard", params={"board": board}, headers=JSON)
|
||||
assert r.status_code == 200, (board, r.text[:200])
|
||||
assert "entries" in r.json()
|
||||
|
||||
|
||||
def test_steal_cooldown_blocks_second_raid(app_server, seeded_db):
|
||||
owner_session, owner = _signup()
|
||||
_reset_farm(owner, coins=100000)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def end_any_active_era():
|
||||
from devplacepy.services.game import store
|
||||
|
||||
if store.active_era():
|
||||
store.end_era()
|
||||
|
||||
|
||||
def test_admin_game_redirect_unauth(page, app_server):
|
||||
page.goto(f"{BASE_URL}/admin/game", wait_until="domcontentloaded")
|
||||
assert page.url.startswith(f"{BASE_URL}/auth/login")
|
||||
|
||||
|
||||
def test_admin_game_page_loads_dormant(alice):
|
||||
page, _ = alice
|
||||
end_any_active_era()
|
||||
page.goto(f"{BASE_URL}/admin/game", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=Code Farm - Eras")
|
||||
assert page.is_visible("text=No Era is currently running")
|
||||
assert page.locator("#era-name").is_visible()
|
||||
|
||||
|
||||
def test_admin_game_start_and_end_era(alice):
|
||||
page, _ = alice
|
||||
end_any_active_era()
|
||||
page.goto(f"{BASE_URL}/admin/game", wait_until="domcontentloaded")
|
||||
page.fill("#era-name", "PlaywrightEra")
|
||||
page.fill("#era-duration", "14")
|
||||
page.click("button:has-text('Start Era')")
|
||||
confirm_btn = page.locator(".dialog-confirm")
|
||||
confirm_btn.wait_for(state="visible")
|
||||
confirm_btn.click()
|
||||
page.wait_for_url("**/admin/game", wait_until="domcontentloaded")
|
||||
expect(page.locator("text=PlaywrightEra")).to_be_visible()
|
||||
assert page.locator("button:has-text('End Era')").is_visible()
|
||||
|
||||
page.click("button:has-text('End Era')")
|
||||
confirm_btn = page.locator(".dialog-confirm")
|
||||
confirm_btn.wait_for(state="visible")
|
||||
confirm_btn.click()
|
||||
page.wait_for_url("**/admin/game", wait_until="domcontentloaded")
|
||||
assert page.is_visible("text=No Era is currently running")
|
||||
|
||||
|
||||
def test_admin_game_start_form_hidden_while_era_active(alice):
|
||||
page, _ = alice
|
||||
end_any_active_era()
|
||||
page.goto(f"{BASE_URL}/admin/game", wait_until="domcontentloaded")
|
||||
page.fill("#era-name", "FirstEra")
|
||||
page.click("button:has-text('Start Era')")
|
||||
confirm_btn = page.locator(".dialog-confirm")
|
||||
confirm_btn.wait_for(state="visible")
|
||||
confirm_btn.click()
|
||||
page.wait_for_url("**/admin/game", wait_until="domcontentloaded")
|
||||
|
||||
page.goto(f"{BASE_URL}/admin/game", wait_until="domcontentloaded")
|
||||
assert page.locator("#era-name").count() == 0
|
||||
end_any_active_era()
|
||||
@@ -18,6 +18,9 @@ def reset_farm(username, coins=100000):
|
||||
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_cosmetics").delete(user_uid=uid)
|
||||
get_table("game_market_ticks").delete()
|
||||
clear_game_caches()
|
||||
farm = store.ensure_farm(uid)
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
||||
return {"uid": uid, "username": username}
|
||||
@@ -46,6 +49,24 @@ def set_farm_xp(username, xp):
|
||||
get_table("game_farms").update({"uid": farm["uid"], "xp": xp}, ["uid"])
|
||||
|
||||
|
||||
def set_farm(username, **fields):
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import store
|
||||
|
||||
user = get_table("users").find_one(username=username)
|
||||
farm = store.get_farm(user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], **fields}, ["uid"])
|
||||
|
||||
|
||||
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 open_game(page):
|
||||
page.goto(f"{BASE_URL}/game", wait_until="domcontentloaded")
|
||||
page.locator("[data-game-grid]").first.wait_for(state="visible")
|
||||
@@ -231,6 +252,164 @@ def test_leaderboard_panel_populates(alice):
|
||||
assert page.is_visible("a.game-lb-name:has-text('alice_test')")
|
||||
|
||||
|
||||
def test_infrastructure_hidden_below_prestige_requirement(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test", coins=100_000_000)
|
||||
open_game(page)
|
||||
assert page.is_visible("text=Requires prestige 3")
|
||||
assert page.locator("form[data-game-action='infrastructure']").count() == 0
|
||||
|
||||
|
||||
def test_infrastructure_buy_succeeds_when_eligible(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test", coins=100_000_000)
|
||||
set_farm("alice_test", prestige=5)
|
||||
open_game(page)
|
||||
buy = page.locator("form[data-game-action='infrastructure'] button").first
|
||||
buy.wait_for(state="visible")
|
||||
buy.click()
|
||||
confirm_btn = page.locator(".dialog-confirm")
|
||||
confirm_btn.wait_for(state="visible")
|
||||
confirm_btn.click()
|
||||
expect(page.locator("form[data-game-action='infrastructure']")).to_have_count(0)
|
||||
assert coins(page) == 100_000_000 - 25_000_000
|
||||
|
||||
|
||||
def test_defense_upgrade_spends_coins(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
upgrade = page.locator("form[data-game-action='defense'] button").first
|
||||
upgrade.wait_for(state="visible")
|
||||
upgrade.click()
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("95000")
|
||||
|
||||
|
||||
def test_cosmetics_buy_and_equip_flow(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test", coins=1_000_000)
|
||||
open_game(page)
|
||||
buy = page.locator("form[data-game-action='cosmetic-buy']").first
|
||||
buy.locator("button").wait_for(state="visible")
|
||||
buy.locator("button").click()
|
||||
confirm_btn = page.locator(".dialog-confirm")
|
||||
confirm_btn.wait_for(state="visible")
|
||||
confirm_btn.click()
|
||||
equip = page.locator("form[data-game-action='cosmetic-equip'] button").first
|
||||
equip.wait_for(state="visible")
|
||||
equip.click()
|
||||
expect(page.locator(".game-cosmetics")).to_contain_text("Equipped")
|
||||
|
||||
|
||||
def test_mastery_panel_hidden_without_mastery_points(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
assert page.locator(".game-mastery").count() == 0
|
||||
|
||||
|
||||
def test_mastery_panel_visible_and_functional_once_unlocked(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
set_farm("alice_test", mastery_points=1000, mastery_points_earned_total=1)
|
||||
open_game(page)
|
||||
upgrade = page.locator(
|
||||
"form[data-game-action='mastery']:has(input[value='autoreplant']) button"
|
||||
)
|
||||
upgrade.wait_for(state="visible")
|
||||
upgrade.click()
|
||||
expect(
|
||||
page.locator(".perk-card:has-text('Continuous Delivery') .perk-level")
|
||||
).to_have_text("Lv 1/1")
|
||||
|
||||
|
||||
def test_leaderboard_board_selector_switches_boards(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
set_farm("alice_test", prestige=7)
|
||||
open_game(page)
|
||||
page.locator(".game-lb-row").first.wait_for(state="visible")
|
||||
page.select_option("[data-lb-board]", "prestige")
|
||||
page.wait_for_timeout(400)
|
||||
page.locator(".game-lb-row").first.wait_for(state="visible")
|
||||
assert page.is_visible("a.game-lb-name:has-text('alice_test')")
|
||||
|
||||
|
||||
def test_underdog_banner_shows_after_raiding_a_much_richer_farm(bob):
|
||||
page, _ = bob
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import store
|
||||
|
||||
reset_farm("alice_test", coins=1_000_000)
|
||||
reset_farm("bob_test", coins=0)
|
||||
alice_user = get_table("users").find_one(username="alice_test")
|
||||
bob_user = get_table("users").find_one(username="bob_test")
|
||||
store.plant({"uid": alice_user["uid"], "username": "alice_test"}, 0, "shell")
|
||||
past = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
|
||||
farm = store.get_farm(alice_user["uid"])
|
||||
plot = store._plot_at(farm["uid"], 0)
|
||||
get_table("game_plots").update(
|
||||
{"uid": plot["uid"], "planted_at": past, "ready_at": past}, ["uid"]
|
||||
)
|
||||
store.steal({"uid": bob_user["uid"], "username": "bob_test"}, alice_user, 0)
|
||||
page.goto(f"{BASE_URL}/game", wait_until="domcontentloaded")
|
||||
page.locator("[data-underdog-banner]").wait_for(state="visible")
|
||||
|
||||
|
||||
def test_weekly_contract_appears_and_can_be_claimed(alice):
|
||||
page, _ = alice
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import store
|
||||
from devplacepy.services.game.store.common import _iso_week
|
||||
from devplacepy.services.game.store.quests import ensure_weekly_contract
|
||||
|
||||
reset_farm("alice_test")
|
||||
set_farm("alice_test", mastery_points=1000)
|
||||
user = get_table("users").find_one(username="alice_test")
|
||||
store.upgrade_mastery({"uid": user["uid"], "username": "alice_test"}, "contracts")
|
||||
farm = store.get_farm(user["uid"])
|
||||
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"]
|
||||
)
|
||||
open_game(page)
|
||||
claim = page.locator(
|
||||
"form[data-game-action='claim-quest']:has(input[value='weekly']) button"
|
||||
)
|
||||
claim.wait_for(state="visible")
|
||||
claim.click()
|
||||
page.wait_for_timeout(400)
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert int(farm["stars"]) > 0
|
||||
|
||||
|
||||
def test_crop_shows_saturated_label_when_overfarmed(alice):
|
||||
page, _ = alice
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game.store import market as market_store
|
||||
from devplacepy.utils import generate_uid
|
||||
|
||||
reset_farm("alice_test")
|
||||
now = datetime.now(timezone.utc)
|
||||
get_table("game_market_ticks").insert(
|
||||
{
|
||||
"uid": generate_uid(),
|
||||
"crop_key": "shell",
|
||||
"hour_bucket": market_store._hour_bucket(now),
|
||||
"harvests": 600,
|
||||
"updated_at": now.isoformat(),
|
||||
}
|
||||
)
|
||||
open_game(page)
|
||||
option = page.locator("form[data-game-action='plant'] select option[value='shell']").first
|
||||
option.wait_for(state="attached")
|
||||
assert "saturated" in option.inner_text()
|
||||
|
||||
|
||||
def test_mobile_no_horizontal_overflow(mobile_page):
|
||||
page, _ = mobile_page
|
||||
reset_farm("alice_test")
|
||||
|
||||
@@ -225,3 +225,177 @@ def test_is_golden_deterministic_and_bounded():
|
||||
def test_is_golden_requires_both_args():
|
||||
assert economy.is_golden("", "2026-06-20T00:00:00+00:00") is False
|
||||
assert economy.is_golden("plot-7", "") is False
|
||||
|
||||
|
||||
# --- market saturation --------------------------------------------------------
|
||||
|
||||
|
||||
def test_market_saturation_factor_is_full_when_fresh():
|
||||
assert economy.market_saturation_factor(0) == 1.0
|
||||
|
||||
|
||||
def test_market_saturation_factor_steps_down():
|
||||
assert economy.market_saturation_factor(40) < 1.0
|
||||
assert economy.market_saturation_factor(600) == 0.40
|
||||
assert economy.market_saturation_factor(40) > economy.market_saturation_factor(600)
|
||||
|
||||
|
||||
def test_market_buff_factor_only_applies_to_starter_crops():
|
||||
assert economy.market_buff_factor("kernel", 0.40) == 1.0
|
||||
assert economy.market_buff_factor("shell", 1.0) == 1.0
|
||||
assert economy.market_buff_factor("shell", 0.40) > 1.0
|
||||
assert economy.market_buff_factor("shell", 0.0) <= economy.MARKET_BUFF_CAP
|
||||
|
||||
|
||||
def test_effective_reward_coins_applies_market_factor():
|
||||
crop = economy.crop_for("shell")
|
||||
full = economy.effective_reward_coins(crop, 0, 0, 0, 1.0)
|
||||
saturated = economy.effective_reward_coins(crop, 0, 0, 0, 0.5)
|
||||
assert saturated < full
|
||||
assert saturated == round(crop.reward_coins * 0.5)
|
||||
|
||||
|
||||
def test_effective_reward_coins_underdog_multiplier():
|
||||
crop = economy.crop_for("shell")
|
||||
plain = economy.effective_reward_coins(crop, 0, 0, 0, 1.0, False)
|
||||
boosted = economy.effective_reward_coins(crop, 0, 0, 0, 1.0, True)
|
||||
assert boosted == round(crop.reward_coins * economy.UNDERDOG_MULTIPLIER)
|
||||
assert boosted > plain
|
||||
|
||||
|
||||
# --- infrastructure and defense ------------------------------------------------
|
||||
|
||||
|
||||
def test_infra_for_known_and_unknown():
|
||||
assert economy.infra_for("registry") is not None
|
||||
assert economy.infra_for("does_not_exist") is None
|
||||
|
||||
|
||||
def test_registry_boost_speeds_only_named_crops():
|
||||
kernel = economy.crop_for("kernel")
|
||||
shell = economy.crop_for("shell")
|
||||
assert economy.grow_seconds_for(kernel, 1, 0, 0, True) < economy.grow_seconds_for(
|
||||
kernel, 1, 0, 0, False
|
||||
)
|
||||
assert economy.grow_seconds_for(shell, 1, 0, 0, True) == economy.grow_seconds_for(
|
||||
shell, 1, 0, 0, False
|
||||
)
|
||||
|
||||
|
||||
def test_defense_tier_lookup_and_progression():
|
||||
assert economy.defense_tier(0).name == "Undefended"
|
||||
assert economy.defense_tier(0).upkeep_daily == 0
|
||||
top = economy.defense_tier(economy.MAX_DEFENSE_LEVEL)
|
||||
assert economy.next_defense_tier(economy.MAX_DEFENSE_LEVEL) is None
|
||||
assert economy.next_defense_tier(0) is not None
|
||||
assert top.steal_fraction_floor < economy.defense_tier(0).steal_fraction_floor
|
||||
|
||||
|
||||
def test_daily_upkeep_scales_with_wealth():
|
||||
tier = economy.defense_tier(1)
|
||||
assert economy.daily_upkeep(tier, 0) == tier.upkeep_daily
|
||||
rich = economy.daily_upkeep(tier, 10_000_000)
|
||||
assert rich > tier.upkeep_daily
|
||||
assert rich == round(10_000_000 * economy.UPKEEP_WEALTH_PCT)
|
||||
|
||||
|
||||
def test_effective_steal_fraction_floor_overridden_by_building():
|
||||
assert economy.effective_steal_fraction(99, floor=0.30) == 0.30
|
||||
assert economy.effective_steal_fraction(0, floor=0.30) == economy.STEAL_FRACTION
|
||||
|
||||
|
||||
def test_effective_steal_grace_extra_seconds_from_building():
|
||||
base = economy.effective_steal_grace(0, 0)
|
||||
with_building = economy.effective_steal_grace(0, 30)
|
||||
assert with_building == base + 30
|
||||
|
||||
|
||||
# --- cosmetics ------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_cosmetic_for_known_and_unknown():
|
||||
assert economy.cosmetic_for("title_architect") is not None
|
||||
assert economy.cosmetic_for("does_not_exist") is None
|
||||
|
||||
|
||||
def test_cosmetic_title_name_only_resolves_titles():
|
||||
assert economy.cosmetic_title_name("title_architect") == "The Architect"
|
||||
assert economy.cosmetic_title_name("skin_neon") == ""
|
||||
assert economy.cosmetic_title_name("") == ""
|
||||
assert economy.cosmetic_title_name("does_not_exist") == ""
|
||||
|
||||
|
||||
# --- mastery --------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_mastery_points_awarded_first_point_at_unlock():
|
||||
assert economy.mastery_points_awarded(49, 50) == 1
|
||||
assert economy.mastery_points_awarded(0, 49) == 0
|
||||
assert economy.mastery_points_awarded(55, 56) == 0
|
||||
assert economy.mastery_points_awarded(59, 60) == 1
|
||||
assert economy.mastery_points_awarded(50, 70) == 2
|
||||
|
||||
|
||||
def test_mastery_for_known_and_unknown():
|
||||
assert economy.mastery_for("autoreplant") is not None
|
||||
assert economy.mastery_for("does_not_exist") is None
|
||||
|
||||
|
||||
def test_mastery_cost_grows_with_level():
|
||||
m = economy.mastery_for("contracts")
|
||||
assert economy.mastery_cost(m, 0) == m.base_cost
|
||||
|
||||
|
||||
def test_unlocked_crops_gates_on_mastery_and_level():
|
||||
base = economy.unlocked_crops(economy.MAX_LEVEL, mastery_earned=0)
|
||||
with_mastery = economy.unlocked_crops(economy.MAX_LEVEL, mastery_earned=1)
|
||||
assert "distsys" not in {c.key for c in base}
|
||||
assert "distsys" in {c.key for c in with_mastery}
|
||||
|
||||
|
||||
def test_crop_payload_locked_by_mastery():
|
||||
distsys = economy.crop_for("distsys")
|
||||
assert economy.crop_payload(distsys, 1, economy.MAX_LEVEL, mastery_earned=0)["locked"] is True
|
||||
assert economy.crop_payload(distsys, 1, economy.MAX_LEVEL, mastery_earned=1)["locked"] is False
|
||||
|
||||
|
||||
def test_secfort_crop_is_steal_immune():
|
||||
secfort = economy.crop_for("secfort")
|
||||
assert secfort.steal_immune is True
|
||||
assert economy.crop_for("shell").steal_immune is False
|
||||
|
||||
|
||||
# --- secondary leaderboard scoring -----------------------------------------------
|
||||
|
||||
|
||||
def test_fair_play_score_rewards_activity_and_penalizes_hoarding():
|
||||
active = economy.fair_play_score(harvests_week=20, coins=0)
|
||||
hoarder = economy.fair_play_score(harvests_week=0, coins=10_000_000)
|
||||
assert active > hoarder
|
||||
|
||||
|
||||
# --- era --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_era_score_gives_prestige_partial_weight():
|
||||
veteran = economy.era_score(era_coins=0, era_harvests=0, prestige=10)
|
||||
rookie = economy.era_score(era_coins=0, era_harvests=0, prestige=0)
|
||||
assert veteran > rookie
|
||||
assert veteran == round(10 * economy.SCORE_PRESTIGE * economy.ERA_PRESTIGE_CARRYOVER_PCT)
|
||||
|
||||
|
||||
def test_era_reward_stars_by_rank():
|
||||
assert economy.era_reward_stars(1) == economy.ERA_REWARD_STARS_BY_RANK[0]
|
||||
assert economy.era_reward_stars(len(economy.ERA_REWARD_STARS_BY_RANK) + 1) == 0
|
||||
assert economy.era_reward_stars(0) == 0
|
||||
|
||||
|
||||
# --- weekly contracts ---------------------------------------------------------------
|
||||
|
||||
|
||||
def test_weekly_contract_deterministic():
|
||||
first = economy.weekly_contract("user-xyz", "2026-W10")
|
||||
second = economy.weekly_contract("user-xyz", "2026-W10")
|
||||
assert first == second
|
||||
assert first["kind"] in economy.QUEST_DEFS
|
||||
assert first["reward_stars"] > 0
|
||||
|
||||
@@ -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