forked from retoor/devplacepy
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>
|
||||
|
||||
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()
|
||||
Reference in New Issue
Block a user