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,52 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import requests
|
||||
|
||||
from devplacepy.services.game import store
|
||||
from tests.api.game.index import JSON, _reset_farm, _signup
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def test_view_farm_public_json(app_server, seeded_db):
|
||||
_reset_farm("alice_test")
|
||||
response = requests.get(f"{BASE_URL}/game/farm/alice_test", headers=JSON)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["farm"]["owner_username"] == "alice_test"
|
||||
|
||||
|
||||
def test_view_unknown_farm_returns_404(app_server, seeded_db):
|
||||
response = requests.get(f"{BASE_URL}/game/farm/nobody_xyz", headers=JSON)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_water_requires_auth(app_server, seeded_db):
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/game/farm/alice_test/water", data={"slot": 0}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_water_neighbour_build(app_server, seeded_db):
|
||||
alice = _reset_farm("alice_test")
|
||||
store.plant({"uid": alice["uid"], "username": "alice_test"}, 0, "python")
|
||||
session, _ = _signup()
|
||||
response = session.post(
|
||||
f"{BASE_URL}/game/farm/alice_test/water", data={"slot": 0}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["farm"]["plots"][0]["watered_count"] == 1
|
||||
|
||||
|
||||
def test_water_own_build_returns_400(app_server, seeded_db):
|
||||
alice = _reset_farm("alice_test")
|
||||
store.plant({"uid": alice["uid"], "username": "alice_test"}, 0, "python")
|
||||
session = requests.Session()
|
||||
session.post(
|
||||
f"{BASE_URL}/auth/login",
|
||||
data={"email": "alice@test.devplace", "password": "secret123"},
|
||||
allow_redirects=True,
|
||||
)
|
||||
response = session.post(
|
||||
f"{BASE_URL}/game/farm/alice_test/water", data={"slot": 0}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 400
|
||||
@@ -0,0 +1,153 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import economy, store
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
JSON = {"Accept": "application/json"}
|
||||
_counter = [0]
|
||||
|
||||
|
||||
def _signup():
|
||||
_counter[0] += 1
|
||||
name = f"gapi{int(time.time() * 1000)}{_counter[0]}"
|
||||
session = requests.Session()
|
||||
session.post(
|
||||
f"{BASE_URL}/auth/signup",
|
||||
data={
|
||||
"username": name,
|
||||
"email": f"{name}@t.dev",
|
||||
"password": "secret123",
|
||||
"confirm_password": "secret123",
|
||||
},
|
||||
allow_redirects=True,
|
||||
)
|
||||
return session, name
|
||||
|
||||
|
||||
def _reset_farm(username, coins=100000):
|
||||
user = get_table("users").find_one(username=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 test_state_requires_auth(app_server, seeded_db):
|
||||
response = requests.get(f"{BASE_URL}/game/state", headers=JSON)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_state_returns_farm_json(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
response = session.get(f"{BASE_URL}/game/state", headers=JSON)
|
||||
assert response.status_code == 200
|
||||
farm = response.json()["farm"]
|
||||
for key in (
|
||||
"coins",
|
||||
"level",
|
||||
"ci_tier",
|
||||
"plots",
|
||||
"crops",
|
||||
"perks",
|
||||
"quests",
|
||||
"prestige",
|
||||
"streak",
|
||||
"daily_available",
|
||||
):
|
||||
assert key in farm
|
||||
assert len(farm["crops"]) == len(economy.CROPS)
|
||||
assert len(farm["perks"]) == len(economy.PERKS)
|
||||
assert len(farm["quests"]) == economy.DAILY_QUEST_COUNT
|
||||
|
||||
|
||||
def test_leaderboard_is_public_json(app_server, seeded_db):
|
||||
response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
|
||||
assert response.status_code == 200
|
||||
assert "entries" in response.json()
|
||||
|
||||
|
||||
def test_plant_returns_updated_farm(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
response = session.post(
|
||||
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 200
|
||||
farm = response.json()["farm"]
|
||||
assert farm["coins"] == 100000 - economy.crop_for("shell").cost
|
||||
assert any(plot["state"] == "growing" for plot in farm["plots"])
|
||||
|
||||
|
||||
def test_plant_requires_auth(app_server, seeded_db):
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_plant_insufficient_coins_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=0)
|
||||
response = session.post(
|
||||
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "error" in response.json()
|
||||
|
||||
|
||||
def test_harvest_empty_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
response = session.post(
|
||||
f"{BASE_URL}/game/harvest", data={"slot": 0}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_buy_plot_and_upgrade_ci(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
buy = session.post(f"{BASE_URL}/game/buy-plot", headers=JSON)
|
||||
assert buy.status_code == 200
|
||||
assert buy.json()["farm"]["plot_count"] == economy.STARTING_PLOTS + 1
|
||||
upgrade = session.post(f"{BASE_URL}/game/upgrade", headers=JSON)
|
||||
assert upgrade.status_code == 200
|
||||
assert upgrade.json()["farm"]["ci_tier"] == 2
|
||||
|
||||
|
||||
def test_daily_claim_then_double_claim_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
first = session.post(f"{BASE_URL}/game/daily", headers=JSON)
|
||||
assert first.status_code == 200
|
||||
assert first.json()["farm"]["streak"] == 1
|
||||
second = session.post(f"{BASE_URL}/game/daily", headers=JSON)
|
||||
assert second.status_code == 400
|
||||
|
||||
|
||||
def test_upgrade_perk_returns_updated_farm(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
response = session.post(
|
||||
f"{BASE_URL}/game/perk", data={"perk": "growth"}, headers=JSON
|
||||
)
|
||||
assert response.status_code == 200
|
||||
growth = next(p for p in response.json()["farm"]["perks"] if p["key"] == "growth")
|
||||
assert growth["level"] == 1
|
||||
|
||||
|
||||
def test_prestige_below_level_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
response = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
|
||||
assert response.status_code == 400
|
||||
+2
-2
@@ -130,8 +130,8 @@ def test_non_ajax_vote_redirects_to_referer(app_server):
|
||||
r = s_b.post(
|
||||
f"{BASE_URL}/votes/post/{post_uid}",
|
||||
data={"value": "1"},
|
||||
headers={"Referer": f"{BASE_URL}/feed"},
|
||||
headers={"Referer": f"{BASE_URL}/gists"},
|
||||
allow_redirects=False,
|
||||
)
|
||||
assert r.status_code == 302
|
||||
assert r.headers["location"] == f"{BASE_URL}/feed"
|
||||
assert r.headers["location"] == "/gists"
|
||||
|
||||
@@ -32,6 +32,7 @@ os.environ["DEVPLACE_WEB_WORKERS"] = "1"
|
||||
# the limiter directly.
|
||||
os.environ["DEVPLACE_DISABLE_RATE_LIMIT"] = "1"
|
||||
os.environ["DEVPLACE_SITEMAP_TTL"] = "0"
|
||||
os.environ["DEVPLACE_HOME_CACHE_TTL"] = "0"
|
||||
|
||||
|
||||
def run_async(coro):
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from tests.conftest import BASE_URL
|
||||
from tests.e2e.game.index import reset_farm
|
||||
|
||||
|
||||
def _plant_growing(username, crop="python", slot=0):
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import store
|
||||
|
||||
user = get_table("users").find_one(username=username)
|
||||
store.plant({"uid": user["uid"], "username": username}, slot, crop)
|
||||
|
||||
|
||||
def test_view_other_farm(bob):
|
||||
page, _ = bob
|
||||
reset_farm("alice_test")
|
||||
page.goto(f"{BASE_URL}/game/farm/alice_test", wait_until="domcontentloaded")
|
||||
page.locator(".game-header h1").wait_for(state="visible")
|
||||
assert "alice_test" in page.locator(".game-header h1").inner_text()
|
||||
assert page.locator("[data-game-grid]").first.is_visible()
|
||||
|
||||
|
||||
def test_water_neighbour_build(bob):
|
||||
page, _ = bob
|
||||
reset_farm("alice_test")
|
||||
_plant_growing("alice_test")
|
||||
page.goto(f"{BASE_URL}/game/farm/alice_test", wait_until="domcontentloaded")
|
||||
page.locator("[data-game-grid]").first.wait_for(state="visible")
|
||||
page.wait_for_timeout(800)
|
||||
water = page.locator("form[data-game-action='water'] button").first
|
||||
water.wait_for(state="visible")
|
||||
water.click()
|
||||
expect(page.locator(".plot-water-count").first).to_contain_text("1/3")
|
||||
|
||||
|
||||
def test_guest_can_view_farm(page):
|
||||
reset_farm("alice_test")
|
||||
page.goto(f"{BASE_URL}/game/farm/alice_test", wait_until="domcontentloaded")
|
||||
page.wait_for_timeout(500)
|
||||
assert "/auth/login" not in page.url
|
||||
assert "alice_test" in page.locator(".game-header h1").inner_text()
|
||||
|
||||
|
||||
def test_unknown_farm_is_404(bob):
|
||||
page, _ = bob
|
||||
response = page.goto(
|
||||
f"{BASE_URL}/game/farm/nope_nobody", wait_until="domcontentloaded"
|
||||
)
|
||||
assert response is not None and response.status == 404
|
||||
@@ -0,0 +1,217 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from playwright.sync_api import expect
|
||||
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def reset_farm(username, coins=100000):
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import store
|
||||
|
||||
user = get_table("users").find_one(username=username)
|
||||
uid = user["uid"]
|
||||
farm = store.get_farm(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(uid)
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
||||
return {"uid": uid, "username": username}
|
||||
|
||||
|
||||
def warp_ready(username):
|
||||
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"])
|
||||
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 set_farm_xp(username, xp):
|
||||
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"], "xp": xp}, ["uid"])
|
||||
|
||||
|
||||
def open_game(page):
|
||||
page.goto(f"{BASE_URL}/game", wait_until="domcontentloaded")
|
||||
page.locator("[data-game-grid]").first.wait_for(state="visible")
|
||||
page.wait_for_timeout(800)
|
||||
|
||||
|
||||
def coins(page):
|
||||
return int(page.locator("[data-hud-coins]").inner_text())
|
||||
|
||||
|
||||
def test_game_page_loads(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
assert page.locator(".game-header h1").is_visible()
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("100000")
|
||||
expect(page.locator("[data-game-grid] [data-slot]")).to_have_count(4)
|
||||
expect(page.locator(".perk-card")).to_have_count(4)
|
||||
expect(page.locator(".quest-item")).to_have_count(3)
|
||||
|
||||
|
||||
def test_guest_redirected_to_login(page):
|
||||
page.goto(f"{BASE_URL}/game", wait_until="domcontentloaded")
|
||||
page.wait_for_timeout(500)
|
||||
assert "/auth/login" in page.url
|
||||
|
||||
|
||||
def test_plant_crop(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
form = page.locator("form[data-game-action='plant']").first
|
||||
form.locator("select[name='crop']").select_option("shell")
|
||||
form.locator("button").click()
|
||||
expect(page.locator(".game-plot-growing")).to_have_count(1)
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("99995")
|
||||
|
||||
|
||||
def test_harvest_crop(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
form = page.locator("form[data-game-action='plant']").first
|
||||
form.locator("select[name='crop']").select_option("shell")
|
||||
form.locator("button").click()
|
||||
expect(page.locator(".game-plot-growing")).to_have_count(1)
|
||||
warp_ready("alice_test")
|
||||
open_game(page)
|
||||
page.locator("form[data-game-action='harvest'] button").first.click()
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("100006")
|
||||
expect(page.locator(".game-plot-growing")).to_have_count(0)
|
||||
|
||||
|
||||
def test_fertilize_spends_coins(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
form = page.locator("form[data-game-action='plant']").first
|
||||
form.locator("select[name='crop']").select_option("python")
|
||||
form.locator("button").click()
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("99985")
|
||||
fert = page.locator("form[data-game-action='fertilize'] button").first
|
||||
fert.wait_for(state="visible")
|
||||
fert.click()
|
||||
expect(page.locator("[data-hud-coins]")).not_to_have_text("99985")
|
||||
assert coins(page) < 99985
|
||||
|
||||
|
||||
def test_buy_plot(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
page.locator("form[data-game-action='buy-plot'] button").click()
|
||||
expect(page.locator("[data-game-grid] [data-slot]")).to_have_count(5)
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("99900")
|
||||
|
||||
|
||||
def test_upgrade_ci(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
page.locator("form[data-game-action='upgrade'] button").click()
|
||||
expect(page.locator("[data-hud-ci]")).to_have_text("2")
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("99850")
|
||||
|
||||
|
||||
def test_claim_daily(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
page.locator("form[data-game-action='daily'] button").click()
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("100020")
|
||||
expect(page.locator(".daily-claimed")).to_be_visible()
|
||||
|
||||
|
||||
def test_upgrade_perk(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
growth = page.locator(
|
||||
"form[data-game-action='perk']:has(input[value='growth']) button"
|
||||
)
|
||||
growth.click()
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("99850")
|
||||
expect(
|
||||
page.locator(".perk-card:has-text('Build Cache') .perk-level")
|
||||
).to_have_text("Lv 1/10")
|
||||
|
||||
|
||||
def test_claim_quest(alice):
|
||||
page, _ = alice
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import store
|
||||
|
||||
farm_info = reset_farm("alice_test")
|
||||
open_game(page)
|
||||
farm = store.get_farm(farm_info["uid"])
|
||||
quest = list(get_table("game_quests").find(farm_uid=farm["uid"]))[0]
|
||||
get_table("game_quests").update(
|
||||
{"uid": quest["uid"], "progress": quest["goal"]}, ["uid"]
|
||||
)
|
||||
open_game(page)
|
||||
page.locator("form[data-game-action='claim-quest'] button").first.click()
|
||||
expect(page.locator("[data-hud-coins]")).not_to_have_text("100000")
|
||||
assert coins(page) > 100000
|
||||
|
||||
|
||||
def test_prestige_hidden_below_level(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
assert page.locator("form[data-game-action='prestige']").count() == 0
|
||||
assert page.is_visible("text=Reach level 10 to refactor")
|
||||
|
||||
|
||||
def test_prestige_resets_farm(alice):
|
||||
page, _ = alice
|
||||
from devplacepy.services.game import economy
|
||||
|
||||
reset_farm("alice_test")
|
||||
set_farm_xp("alice_test", economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
open_game(page)
|
||||
prestige = page.locator("form[data-game-action='prestige'] button")
|
||||
prestige.wait_for(state="visible")
|
||||
prestige.click()
|
||||
confirm_btn = page.locator(".dialog-confirm")
|
||||
confirm_btn.wait_for(state="visible")
|
||||
confirm_btn.click()
|
||||
expect(page.locator("[data-hud-prestige]")).to_have_text("1")
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text("50")
|
||||
assert page.locator("form[data-game-action='prestige']").count() == 0
|
||||
|
||||
|
||||
def test_leaderboard_panel_populates(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
page.locator(".game-lb-row").first.wait_for(state="visible")
|
||||
assert page.is_visible("a.game-lb-name:has-text('alice_test')")
|
||||
|
||||
|
||||
def test_mobile_no_horizontal_overflow(mobile_page):
|
||||
page, _ = mobile_page
|
||||
reset_farm("alice_test")
|
||||
open_game(page)
|
||||
scroll_w = page.evaluate("document.documentElement.scrollWidth")
|
||||
client_w = page.evaluate("document.documentElement.clientWidth")
|
||||
assert scroll_w - client_w <= 1, f"horizontal overflow: {scroll_w} vs {client_w}"
|
||||
assert page.locator("[data-game-grid]").first.is_visible()
|
||||
@@ -77,3 +77,31 @@ def test_block_mute_tools_hidden_from_guests():
|
||||
names = {s["function"]["name"] for s in schemas}
|
||||
for name in ("block_user", "unblock_user", "mute_user", "unmute_user"):
|
||||
assert name not in names
|
||||
|
||||
|
||||
def test_game_actions_registered_with_correct_auth():
|
||||
public = {"game_state", "game_leaderboard", "game_view_farm"}
|
||||
auth = {
|
||||
"game_plant",
|
||||
"game_harvest",
|
||||
"game_buy_plot",
|
||||
"game_upgrade_ci",
|
||||
"game_water",
|
||||
"game_fertilize",
|
||||
"game_daily",
|
||||
"game_upgrade_perk",
|
||||
"game_claim_quest",
|
||||
"game_prestige",
|
||||
}
|
||||
for name in public | auth:
|
||||
assert name in BY_NAME, name
|
||||
assert BY_NAME["game_state"].requires_auth is True
|
||||
assert BY_NAME["game_leaderboard"].requires_auth is False
|
||||
assert BY_NAME["game_view_farm"].requires_auth is False
|
||||
for name in auth:
|
||||
assert BY_NAME[name].requires_auth is True, name
|
||||
|
||||
|
||||
def test_game_read_actions_are_read_only():
|
||||
for name in ("game_state", "game_leaderboard", "game_view_farm"):
|
||||
assert BY_NAME[name].is_read_only is True
|
||||
|
||||
@@ -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")
|
||||
@@ -221,7 +221,11 @@ def test_run_once_updates_existing_news_row(local_db, monkeypatch):
|
||||
assert get_table("news").count(external_id=external_id) == 1
|
||||
|
||||
|
||||
def test_get_article_images_filters_and_dedupes():
|
||||
def test_get_article_images_filters_and_dedupes(monkeypatch):
|
||||
async def _allow(url):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(news_mod.net_guard, "guard_public_url", _allow)
|
||||
html = (
|
||||
'<img src="http://img.test/a.png">'
|
||||
"<img src='http://img.test/a.png'>"
|
||||
|
||||
Reference in New Issue
Block a user