parent
64c3983c9f
commit
582e37d176
@ -125,8 +125,9 @@ The **Code Farm** (`/game`) is a cooperative idle game in the spirit of Farmvill
|
||||
- **Daily bonus.** Claim a coin bonus once per day; consecutive days build a streak that grows the reward (capped at seven days).
|
||||
- **Daily quests.** Three quests rotate every day (plant, harvest, water, or earn goals), tracked automatically as you play; claim each one for coins and XP when complete.
|
||||
- **Perks.** Spend coins on four permanent upgrades - Optimizer (+harvest coins), Build Cache (+build speed), Bulk Licenses (-planting cost), and Mentorship (+harvest XP) - each levelling up with escalating cost.
|
||||
- **Refactor (prestige).** At level 10 you can refactor: the farm resets (coins, level, CI, extra plots, perks) in exchange for a permanent +25% coin bonus that stacks with every refactor. Each refactor also awards **Stars** (scaled by the level and prestige you reached) to spend in the Legacy shop.
|
||||
- **Stars and Legacy (endgame).** Stars buy permanent **Legacy** upgrades that survive every refactor, unlike perks: **CI Bot** (auto-collects ready builds when you open your farm), **Tech Debt Payoff** (+coins, stacks with prestige), **Bare-Metal** (+base build speed), **Monorepo** (+starting plots after each refactor), and **Branch Protection** (longer steal grace and a smaller steal cut). This is the infinite progression for maxed farms, and CI Bot makes the game playable hands-off.
|
||||
- **Refactor (prestige).** At level 10 you can refactor: the farm resets (coins, level, CI, extra plots, perks) in exchange for a permanent +25% coin bonus that stacks with every refactor. Each refactor also awards **Stars** (scaled by the level and prestige you reached) to spend in the Legacy shop. Refactoring costs a **dynamic coin fee** that grows with your prestige and current wealth, so you must farm well past level 10 to afford each one - and 10% of what remains after the fee (more with the Golden Parachute Legacy upgrade, up to 35%) carries over into the new run.
|
||||
- **Community treasury and weekly grant.** Every refactor fee flows into a shared treasury. Active low-balance, low-prestige farms (at least five harvests this week, under 10,000 coins, at most prestige 5) can claim a capped grant from it once per week - a direct wealth transfer from the farms refactoring at the top to the farms building at the bottom.
|
||||
- **Stars and Legacy (endgame).** Stars buy permanent **Legacy** upgrades that survive every refactor, unlike perks: **CI Bot** (auto-collects ready builds when you open your farm), **Tech Debt Payoff** (+coins, stacks with prestige), **Bare-Metal** (+base build speed), **Monorepo** (+starting plots after each refactor), **Branch Protection** (longer steal grace and a smaller steal cut), and **Golden Parachute** (a larger refactor coin carry-over). This is the infinite progression for maxed farms, and CI Bot makes the game playable hands-off.
|
||||
- **Golden builds.** A small share of plantings come out golden (marked with a sparkle); harvesting a golden build pays several times the coins.
|
||||
- **Visit and water friends.** Open another member's farm at `/game/farm/{username}` and water their growing builds to speed them up - you earn coins for helping, and the owner sees the help live. This is the social loop that makes the game cooperative.
|
||||
- **Steal a harvest.** A ready build on someone else's farm can be stolen once a protection window passes - the owner gets that grace period (longer if they invested in Branch Protection or a Defense building) to harvest it first. A successful steal pays the thief a fraction of the build's coin value (the owner loses the whole build) and earns the **Cat Burglar** badge; the victim gets the **Robbed** badge and a live notification that someone raided their farm (the thief is never named). You can raid any given neighbour only **once per hour**. Raiding a farm with 10x your own coins grants a 24-hour **Underdog** boost (+25% coin gain) and the **David vs Goliath** badge. Stealing pays coins only, so the harvest-based leaderboards stay earned by real farming.
|
||||
@ -138,7 +139,7 @@ The **Code Farm** (`/game`) is a cooperative idle game in the spirit of Farmvill
|
||||
- **Leaderboards.** Several boards, selectable from the game page: **Overall score** (a composite weighing refactor/prestige count, XP, lifetime harvests, coins, CI tier, plots, perks, and streak), **Prestige**, **Harvests this week**, **Raid efficiency** (average coins per successful raid), **Fastest to Kernel** (time since your last refactor), **Fair play** (rewards recent activity over hoarding), and (when running) the current **Era** board.
|
||||
- **Eras (admin-managed seasons).** Administrators can start a Era at `/admin/game`: every farm's *visible* Era coins/harvests counters reset to zero, but real coin balances, prestige, Stars, Legacy, and Mastery are never touched. Ending an Era ranks farms by Era score (which gives prestige only partial weight, so veterans keep an edge without it being insurmountable), awards Stars to the top 10, and permanently records the results.
|
||||
|
||||
The farm refreshes live over the pub/sub bus (a watered build appears on the owner's screen at once) and every plot countdown ticks client-side. Every endpoint also answers JSON, and Devii can play the game on the member's behalf via the `game_*` tools (`game_state`, `game_plant`, `game_harvest`, `game_buy_plot`, `game_upgrade_ci`, `game_water`, `game_steal`, `game_view_farm`, `game_leaderboard`, `game_upgrade_perk`, `game_upgrade_legacy`, `game_prestige`, `game_upgrade_mastery`, `game_buy_infrastructure`, `game_upgrade_defense`, `game_buy_cosmetic`, `game_equip_cosmetic`). See the API reference group **Code Farm**.
|
||||
The farm refreshes live over the pub/sub bus (a watered build appears on the owner's screen at once) and every plot countdown ticks client-side. Every endpoint also answers JSON, and Devii can play the game on the member's behalf via the `game_*` tools (`game_state`, `game_plant`, `game_harvest`, `game_buy_plot`, `game_upgrade_ci`, `game_water`, `game_steal`, `game_view_farm`, `game_leaderboard`, `game_upgrade_perk`, `game_upgrade_legacy`, `game_prestige`, `game_claim_grant`, `game_upgrade_mastery`, `game_buy_infrastructure`, `game_upgrade_defense`, `game_buy_cosmetic`, `game_equip_cosmetic`). See the API reference group **Code Farm**.
|
||||
|
||||
## Engagement
|
||||
|
||||
|
||||
@ -1043,6 +1043,8 @@ def init_db():
|
||||
("legacy_speed", 0),
|
||||
("legacy_plots", 0),
|
||||
("legacy_defense", 0),
|
||||
("legacy_carryover", 0),
|
||||
("last_grant_week", ""),
|
||||
("prestiged_at", ""),
|
||||
("mastery_points", 0),
|
||||
("mastery_points_earned_total", 0),
|
||||
@ -1170,6 +1172,17 @@ def init_db():
|
||||
unique=True,
|
||||
)
|
||||
|
||||
game_treasury = get_table("game_treasury")
|
||||
for column, example in (
|
||||
("uid", ""),
|
||||
("balance", 0),
|
||||
("collected_total", 0),
|
||||
("granted_total", 0),
|
||||
("updated_at", ""),
|
||||
):
|
||||
if not game_treasury.has_column(column):
|
||||
game_treasury.create_column_by_example(column, example)
|
||||
|
||||
game_eras = get_table("game_eras")
|
||||
for column, example in (
|
||||
("uid", ""),
|
||||
|
||||
@ -12,6 +12,9 @@ The Code Farm is a cooperative idle game. Each member owns a farm of plots, plan
|
||||
projects that build over real time, harvests them for coins and XP, upgrades their CI tier for
|
||||
faster builds, and waters other members' growing builds to speed them up and earn coins.
|
||||
|
||||
Refactoring (prestige) costs a dynamic coin fee that grows with prestige and current wealth;
|
||||
the fees fill a community treasury from which active low-balance farms can claim a weekly grant.
|
||||
|
||||
All endpoints negotiate HTML or JSON. The action endpoints return the full farm state so a
|
||||
client can refresh without a second request.
|
||||
""",
|
||||
@ -180,17 +183,26 @@ client can refresh without a second request.
|
||||
method="POST",
|
||||
path="/game/prestige",
|
||||
title="Refactor (prestige)",
|
||||
summary="Reset the farm at level 10+ for a permanent +25% coin bonus and earn Stars to spend on Legacy upgrades. From prestige 50 onward, every 10 more prestige also earns a permanent Mastery point.",
|
||||
summary="Reset the farm at level 10+ for a permanent +25% coin bonus and earn Stars to spend on Legacy upgrades. Refactoring costs a coin fee that scales with prestige and current wealth (the farm state's refactor_cost); the fee funds the community treasury and a fraction of the remaining coins (10% base, more with the Golden Parachute Legacy upgrade) carries over. From prestige 50 onward, every 10 more prestige also earns a permanent Mastery point.",
|
||||
auth="user",
|
||||
destructive=True,
|
||||
sample_response={"ok": True, "farm": {"prestige": 1}},
|
||||
sample_response={"ok": True, "farm": {"prestige": 1, "coins": 6550}},
|
||||
),
|
||||
endpoint(
|
||||
id="game-grant",
|
||||
method="POST",
|
||||
path="/game/grant",
|
||||
title="Claim the community grant",
|
||||
summary="Claim the weekly community grant, paid from the treasury filled by refactor fees. Eligible farms are active (5+ harvests this week), below 10000 coins, and at most prestige 5.",
|
||||
auth="user",
|
||||
sample_response={"ok": True, "farm": {"coins": 2550}},
|
||||
),
|
||||
endpoint(
|
||||
id="game-legacy",
|
||||
method="POST",
|
||||
path="/game/legacy",
|
||||
title="Buy a Legacy upgrade",
|
||||
summary="Spend Stars on a permanent Legacy upgrade that survives every refactor: autoharvest, multiplier, speed, plots, or defense.",
|
||||
summary="Spend Stars on a permanent Legacy upgrade that survives every refactor: autoharvest, multiplier, speed, plots, defense, or carryover (Golden Parachute, raises the refactor coin carry-over).",
|
||||
auth="user",
|
||||
params=[field("key", "form", "string", True, "multiplier", "Legacy upgrade key.")],
|
||||
sample_response={"ok": True, "farm": {"stars": 1}},
|
||||
|
||||
@ -43,7 +43,7 @@ Prefixes are wired in `main.py`:
|
||||
| `/xmlrpc` | xmlrpc.py - reverse-proxies XML-RPC calls to the forking XML-RPC bridge (`services/xmlrpc/`, supervised by `XmlrpcService` on loopback `config.XMLRPC_PORT`), which generates one XML-RPC method per documented REST endpoint from `docs_api.API_GROUPS` (`posts.create`, `feed.list`, ...). One struct of named params per call; auth via in-band `api_key`, `X-API-KEY`/`Bearer` header, or `http://user:pass@host/xmlrpc` Basic. Full introspection + `system.multicall`; REST errors become XML-RPC faults. Exempt from the rate limiter (enforced on the forwarded internal hop). See `devplacepy/services/xmlrpc/CLAUDE.md` |
|
||||
| `/api` | devrant/ package - devRant-compatible REST protocol (`auth.py`, `rants.py`, `comments.py`, `notifs.py`). Translates devRant requests onto DevPlace data: rants<->posts, comments/votes onto the native engagement layer, devRant integer ids onto each table's auto-increment `id`. Token auth via `devrant_tokens`. See `routers/devrant/CLAUDE.md` for the deep detail on this tree |
|
||||
| `/dbapi` | dbapi/ package - **primary-administrator-only, strictly READ-ONLY** generic database API over `dataset` (never inserts/updates/deletes/restores; the write surface was removed because it bypassed every per-route admin safeguard). `tables.py` (`GET /tables`, `GET /{table}/schema`), `crud.py` (read only: `GET /{table}` + `GET /{table}/{key}/{value}`, `?include_deleted`), `query.py` (`POST /query` hard SELECT-only validated read-only run; `POST /query/async` + `GET /query/{uid}` + `WS /query/{uid}/ws` via `DbApiJobService` kind `dbquery`), `nl.py` (`POST /nl` natural-language->SQL via the AI gateway, returns validated SELECT, `execute=true` runs it read-only). **Auth = the PRIMARY administrator only** (the earliest-created Admin, `utils.is_primary_admin` / `database.get_primary_admin_uid` - the same identity that gates backup downloads) by session/api_key (`services/dbapi/policy.py`); every other administrator is refused like a member, and there is **no internal-key path** - the gateway `internal_gateway_key()` is NOT accepted (no service-to-service access). SQL validated by `services/dbapi/validate.py` (sqlglot classify + suspicious-flag + read-only `EXPLAIN` dry-run). Devii tools `db_*` are read-only (list/get/query/nl; no write tools) and flagged `requires_primary_admin=True`, so they are added to the LLM tool list ONLY for the primary administrator (every other session never sees them and Devii is unaware they exist). `services/pubsub/policy.py` resolves its own admin/internal actor and does NOT reuse `dbapi.policy.caller_for`, so the primary-admin restriction does not leak onto the pub/sub bus. See `devplacepy/services/dbapi/CLAUDE.md` |
|
||||
| `/game` | game/ package - the **Code Farm** idle game (`index.py` + `farm.py`): `GET /game` (page), `GET /game/state`, `GET /game/leaderboard?board=`, `POST /game/{plant,harvest,buy-plot,upgrade,fertilize,daily,perk,prestige,legacy,mastery,quests/claim}`, `POST /game/{defense/upgrade,infrastructure/buy,cosmetics/buy,cosmetics/equip}`, plus social `GET /game/farm/{username}` and `POST /game/farm/{username}/{water,steal}`. See `devplacepy/services/game/CLAUDE.md` |
|
||||
| `/game` | game/ package - the **Code Farm** idle game (`index.py` + `farm.py`): `GET /game` (page), `GET /game/state`, `GET /game/leaderboard?board=`, `POST /game/{plant,harvest,buy-plot,upgrade,fertilize,daily,grant,perk,prestige,legacy,mastery,quests/claim}`, `POST /game/{defense/upgrade,infrastructure/buy,cosmetics/buy,cosmetics/equip}`, plus social `GET /game/farm/{username}` and `POST /game/farm/{username}/{water,steal}`. See `devplacepy/services/game/CLAUDE.md` |
|
||||
| (none) | push.py - web push + PWA: `GET`/`POST /push.json` (VAPID public key / subscription register), `GET /service-worker.js`, `GET /manifest.json` |
|
||||
| (none) | docs.py (docs/ package) - the documentation site (prose pages + API reference). See `routers/docs/CLAUDE.md` for the deep detail on this tree (`DOCS_PAGES`, audience tiers, prose rendering pipeline) |
|
||||
| `/pubsub` | pubsub.py - **database-free** publish/subscribe bus. `WS /pubsub/ws` (subscribe/unsubscribe/publish frames, `foo.*` wildcards), `POST /pubsub/publish` + `GET /pubsub/topics` (admin/internal). Lock-owner-gated WS (`4013` retry) so subscribers converge on one worker; topic authz in `services/pubsub/policy.py` (`user.{uid}.*` private, `public.*` shared, admin/internal anywhere, guests opt-in). In-process `services.pubsub.publish(topic, data)` for backends; frontend `app.pubsub`. See `devplacepy/services/pubsub/CLAUDE.md` |
|
||||
@ -188,7 +188,7 @@ News articles have an internal detail page at `/news/{slug}` with full comment s
|
||||
The home route (`main.py` `landing()`) never redirects - it renders `templates/landing.html` for everyone, branching on `user`:
|
||||
|
||||
- **Guests** get the marketing hero (`Join DevPlace Free` CTA + features grid).
|
||||
- **Signed-in users** get a personalized hero (`.landing-hero-user`): avatar, "Welcome back, {username}", a `Go to your feed` CTA, a Posts/Stars/Level stat strip (`user_post_count` + the user dict's `stars`/`level`), and quick links. Styles live in `.landing-hero-user`/`.landing-welcome`/`.landing-stats`/`.landing-quicklinks` in `static/css/landing.css`.
|
||||
- **Signed-in users** get a personalized dashboard hero (`.dashboard-welcome`): avatar, "Welcome back, {username}", quicklink buttons (`.dashboard-btn`, with `New Post` -> `/feed` as `.dashboard-btn-primary`, plus Code Farm/Projects/Gists), and a Posts/Stars/Level stat strip (`.dashboard-stats`, `user_post_count` + the user dict's `stars`/`level`). Styles live in the `.dashboard-*` classes in `static/css/landing.css`.
|
||||
- Both states share the Latest Posts + Developer News + "Build With Us" sections. The **Build With Us** section is static HTML/CSS (`.landing-help-*` in `landing.css`): four cards linking to `/docs/index.html` (Documentation), `/swagger` + `/openapi.json` (API Reference), `/issues` (Contribute & Report), and Devii. The Devii card's `Launch Devii` button is a plain `<button data-devii-open>` that opens the globally mounted `DeviiTerminal` (`app.devii`) in place - no extra JS, route, schema, or Devii action; a secondary link points to `/devii/` for the full terminal page.
|
||||
- Context adds `user`, `is_authenticated`, `user_post_count`; `LandingOut` carries `is_authenticated`/`user_post_count` for the JSON form. `GET /` is documented in `docs_api.py` (id `home`, mapped to `LandingOut`).
|
||||
|
||||
|
||||
@ -122,6 +122,12 @@ async def game_daily(request: Request):
|
||||
return await _respond_action(request, user, lambda: store.claim_daily(user))
|
||||
|
||||
|
||||
@router.post("/grant")
|
||||
async def game_claim_grant(request: Request):
|
||||
user = require_user(request)
|
||||
return await _respond_action(request, user, lambda: store.claim_grant(user))
|
||||
|
||||
|
||||
@router.post("/perk")
|
||||
async def game_perk(request: Request, data: Annotated[GamePerkForm, Form()]):
|
||||
user = require_user(request)
|
||||
|
||||
@ -134,6 +134,14 @@ class GameFarmOut(_Out):
|
||||
prestige_multiplier: float = 1.0
|
||||
prestige_min_level: int = 0
|
||||
prestige_available: bool = False
|
||||
refactor_cost: int = 0
|
||||
refactor_affordable: bool = False
|
||||
refactor_carryover_pct: int = 0
|
||||
refactor_carryover_preview: int = 0
|
||||
grant_available: bool = False
|
||||
grant_amount: int = 0
|
||||
grant_reason: str = ""
|
||||
treasury_balance: int = 0
|
||||
streak: int = 0
|
||||
daily_available: bool = False
|
||||
daily_reward: int = 0
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from ..spec import Action, Param
|
||||
from ._shared import body, path, query
|
||||
from ._shared import body, confirm, path, query
|
||||
|
||||
|
||||
GAME_ACTIONS: tuple[Action, ...] = (
|
||||
@ -160,7 +160,25 @@ GAME_ACTIONS: tuple[Action, ...] = (
|
||||
name="game_prestige",
|
||||
method="POST",
|
||||
path="/game/prestige",
|
||||
summary="Refactor (prestige) your Code Farm for a permanent coin bonus and Stars (requires level 10)",
|
||||
summary=(
|
||||
"Refactor (prestige) your Code Farm for a permanent coin bonus and Stars. "
|
||||
"Requires level 10 AND a coin fee that scales with prestige and wealth "
|
||||
"(shown as refactor_cost in game_state); the fee funds the community "
|
||||
"treasury and a fraction of the remaining coins carries over. "
|
||||
"Irreversible, confirmation required"
|
||||
),
|
||||
handler="http",
|
||||
requires_auth=True,
|
||||
params=(confirm(),),
|
||||
),
|
||||
Action(
|
||||
name="game_claim_grant",
|
||||
method="POST",
|
||||
path="/game/grant",
|
||||
summary=(
|
||||
"Claim the weekly Code Farm community grant, paid from the treasury of "
|
||||
"refactor fees (active low-balance, low-prestige farms only)"
|
||||
),
|
||||
handler="http",
|
||||
requires_auth=True,
|
||||
),
|
||||
@ -168,13 +186,13 @@ GAME_ACTIONS: tuple[Action, ...] = (
|
||||
name="game_upgrade_legacy",
|
||||
method="POST",
|
||||
path="/game/legacy",
|
||||
summary="Spend Stars on a permanent Legacy upgrade that survives refactor (autoharvest, multiplier, speed, plots, defense)",
|
||||
summary="Spend Stars on a permanent Legacy upgrade that survives refactor (autoharvest, multiplier, speed, plots, defense, carryover)",
|
||||
handler="http",
|
||||
requires_auth=True,
|
||||
params=(
|
||||
body(
|
||||
"key",
|
||||
"Legacy key: autoharvest, multiplier, speed, plots, or defense.",
|
||||
"Legacy key: autoharvest, multiplier, speed, plots, defense, or carryover.",
|
||||
required=True,
|
||||
),
|
||||
),
|
||||
|
||||
@ -61,6 +61,7 @@ CONFIRM_REQUIRED = {
|
||||
"gateway_quota_rule_delete",
|
||||
"email_account_delete",
|
||||
"email_delete_message",
|
||||
"game_prestige",
|
||||
}
|
||||
|
||||
CONDITIONAL_CONFIRM = {
|
||||
|
||||
@ -4,7 +4,7 @@ This file documents the Code Farm idle game. Claude Code auto-loads it when a fi
|
||||
|
||||
## Overview
|
||||
|
||||
`game/` package (routers, mounted at `/game`) is the **Code Farm** idle game (`index.py` + `farm.py`): `GET /game` (page), `GET /game/state`, `GET /game/leaderboard`, `POST /game/{plant,harvest,buy-plot,upgrade,fertilize,daily,perk,prestige,legacy,quests/claim}`, plus social `GET /game/farm/{username}` and `POST /game/farm/{username}/{water,steal}`.
|
||||
`game/` package (routers, mounted at `/game`) is the **Code Farm** idle game (`index.py` + `farm.py`): `GET /game` (page), `GET /game/state`, `GET /game/leaderboard`, `POST /game/{plant,harvest,buy-plot,upgrade,fertilize,daily,grant,perk,prestige,legacy,quests/claim}`, plus social `GET /game/farm/{username}` and `POST /game/farm/{username}/{water,steal}`.
|
||||
|
||||
A cooperative-and-competitive idle game (Farmville-style) mounted at `/game`, member-only to play, public to view another farm. Cooperative loop = watering a neighbour's growing build; competitive loop = stealing a neighbour's ready build.
|
||||
|
||||
@ -14,7 +14,7 @@ A cooperative-and-competitive idle game (Farmville-style) mounted at `/game`, me
|
||||
|
||||
## Tables
|
||||
|
||||
`game_farms` (one per user, `coins`/`xp`/`level`/`ci_tier`/`plot_count`/`total_harvests`, plus `prestige`/`streak`/`last_daily_at`, the four `perk_*` columns, and the endgame `stars` + five `legacy_*` columns), `game_plots` (`farm_uid`/`slot_index`/`crop_key`/`planted_at`/`ready_at`/`watered_by`), and `game_steals` (`thief_uid`/`owner_uid`/`slot_index`/`crop_key`/`coins`/`stolen_at`, the per-pair steal-cooldown ledger) are **not** soft-deletable - they are mutable game state consumed by state transitions, not user content. Columns + indexes are ensured in `database.init_db` (unique `idx_game_farms_user`, `idx_game_farms_rank`, `idx_game_plots_farm`, `idx_game_steals_pair` on `(thief_uid, owner_uid, stolen_at)`); the `_uid_index` loop adds the uid index. `ensure_farm(user_uid)` lazily creates a farm + starting plots on first access (idempotent), so there is no signup hook.
|
||||
`game_farms` (one per user, `coins`/`xp`/`level`/`ci_tier`/`plot_count`/`total_harvests`, plus `prestige`/`streak`/`last_daily_at`/`last_grant_week`, the four `perk_*` columns, and the endgame `stars` + six `legacy_*` columns including `legacy_carryover`), `game_plots` (`farm_uid`/`slot_index`/`crop_key`/`planted_at`/`ready_at`/`watered_by`), `game_steals` (`thief_uid`/`owner_uid`/`slot_index`/`crop_key`/`coins`/`stolen_at`, the per-pair steal-cooldown ledger), and `game_treasury` (a single row `uid="treasury-main"` holding `balance`/`collected_total`/`granted_total`, the refactor-fee redistribution ledger) are **not** soft-deletable - they are mutable game state consumed by state transitions, not user content. Columns + indexes are ensured in `database.init_db` (unique `idx_game_farms_user`, `idx_game_farms_rank`, `idx_game_plots_farm`, `idx_game_steals_pair` on `(thief_uid, owner_uid, stolen_at)`); the `_uid_index` loop adds the uid index. `ensure_farm(user_uid)` lazily creates a farm + starting plots on first access (idempotent), so there is no signup hook.
|
||||
|
||||
## Routes (`routers/game/`)
|
||||
|
||||
@ -42,7 +42,7 @@ Five further systems layer onto the base loop, every one defaulting gracefully f
|
||||
2. **Daily quests** (`game_quests` table, `POST /game/quests/claim`): `economy.daily_quests(user_uid, day)` deterministically (sha256 of `user:day`) picks 3 of {plant, harvest, water, earn} with goals/rewards; `store.ensure_quests` lazily materializes the day's rows, `store.advance_quests(user_uid, kind, amount)` is called inside `plant`/`harvest`/`water` (wrapped in try/except so a quest write never breaks the action), `claim_quest` pays out when `progress >= goal`.
|
||||
3. **Perks** (`POST /game/perk`, `store.upgrade_perk`): four permanent upgrades (`economy.PERKS`) with escalating `perk_cost`; applied in the economy formulas - `effective_plant_cost` (discount), `grow_seconds_for(crop, ci_tier, growth_level)` via `farm_speed` (growth), `effective_reward_coins` (yield + prestige), `effective_reward_xp` (xp).
|
||||
4. **Fertilizer** (`POST /game/fertilize`, `store.fertilize`): cut a growing plot's `ready_at` by `FERTILIZE_FRACTION` for `economy.fertilize_click_cost(eff_reward, reduce_seconds, full_grow_seconds)` = `ceil(eff_reward * reduce/full_grow * FERTILIZE_TAX)` (TAX 1.05). **The cost is priced against the build's realized harvest value (`effective_reward_coins`, which already carries yield/prestige/legacy multipliers), not raw grow-seconds, so the prestige dependence cancels and fully fertilizing a crop always costs >= its harvest - fertilize is a pure time-skip and can NEVER be a profit at any prestige.** (This replaced the old grow-seconds-based `fertilize_cost`, which was an unbounded money pump at high prestige.)
|
||||
5. **Prestige/Refactor** (`POST /game/prestige`, `store.prestige`): at `PRESTIGE_MIN_LEVEL` resets coins/xp/level/ci/perks, sets `plot_count = economy.prestige_base_plots(legacy_plots_level)` (keeps/recreates plot rows up to that base, deletes the rest), increments `prestige` for a permanent `prestige_multiplier` (+25% coins each), and awards `economy.stars_for_refactor(level, prestige)` Stars (the `legacy_*`/`stars` columns are **omitted from the reset dict** so they survive every refactor, the established pattern).
|
||||
5. **Prestige/Refactor** (`POST /game/prestige`, `store.prestige`): at `PRESTIGE_MIN_LEVEL` resets coins/xp/level/ci/perks, sets `plot_count = economy.prestige_base_plots(legacy_plots_level)` (keeps/recreates plot rows up to that base, deletes the rest), increments `prestige` for a permanent `prestige_multiplier` (+25% coins each), and awards `economy.stars_for_refactor(level, prestige)` Stars (the `legacy_*`/`stars` columns are **omitted from the reset dict** so they survive every refactor, the established pattern). **Refactoring costs a dynamic coin fee** - see "Refactor fee, carry-over, treasury, and community grant" below.
|
||||
|
||||
`serialize_farm` exposes all of this (`perks`, `quests`, `streak`, `daily_available`/`daily_reward`, `prestige*`, `stars`, `legacy`, `steal_cooldown_seconds`) only to the owner; the existing `crop_payload`/`serialize_plot` now carry perk- and legacy-adjusted costs/rewards and per-plot value-based `fertilize_cost`. Frontend hosts (`[data-shop-host]`/`[data-perk-host]`/`[data-legacy-host]`/`[data-daily-host]`/`[data-quest-host]`) are server-rendered from partials (`_game_shop.html`/`_game_perks.html`/`_game_legacy.html`/`_game_daily.html`/`_game_quests.html`) and fully re-rendered by `GameFarm.js` builders; the generic `data-game-action` form delegation handles the new actions, with `data-confirm` gating the prestige reset.
|
||||
|
||||
@ -102,6 +102,16 @@ Three new high-tier crops (`distsys`/`mlpipe`/`secfort` in `economy.CROPS`) exte
|
||||
|
||||
**"Crops ready" notifications are client-side only** (`GameFarm.js`'s existing 1-second ticker fires a toast/desktop `Notification` when a countdown hits zero while the tab is open) - this respects the hard "no background tick" architectural invariant, since nothing server-side needs to detect the transition. **"Someone is scouting you" was not built** - like Observability's real-time alert, it would require inventing a scouting/detection mechanic that does not exist. **Alliance/Farm Coop was deliberately deferred** to its own follow-up plan - it is a new multiplayer social feature (group membership, shared resource pooling, its own invite/leave/kick UX), qualitatively different from every economy-tuning change here, and was judged to deserve its own design pass rather than being folded into this rebalance.
|
||||
|
||||
### Refactor fee, carry-over, treasury, and community grant (`store/treasury.py`)
|
||||
|
||||
Refactoring is no longer free at level 10 - it is the economy's progressive wealth sink, and the sink funds a redistribution loop. All backwards compatible: new `game_farms` columns (`legacy_carryover`, `last_grant_week`) default NULL and are `COALESCE`d, and the `game_treasury` table starts empty (lazily seeded by `ensure_treasury`).
|
||||
|
||||
- **Dynamic fee** (`economy.refactor_cost(prestige, coins)` = `REFACTOR_BASE_COST` (20k) `* (1 + prestige) * prestige_multiplier(prestige) + REFACTOR_WEALTH_PCT` (15%) `* coins`): the polynomial prestige term makes time-to-afford grow linearly with prestige (a soft cap on prestige cycling - income grows only linearly, so an exponential fee would wall off Mastery), and the wealth term is a progressive tax a hoarder cannot dodge. At prestige 0 the fee means a level-10 farm must keep farming (~23.5k coins minimum) before its first refactor - instant level-10 cycling is dead.
|
||||
- **Carry-over** (`economy.refactor_carryover(coins, fee, carryover_level)`): after the fee, `REFACTOR_CARRYOVER_BASE` (10%) of the remainder survives the reset, raised +5%/level by the new **Golden Parachute** Legacy upgrade (`legacy_carryover`, max 5 -> 35%). Always strictly below 100% so a refactor can never be net-profitable (same invariant class as the fertilize fix). `prestige()` pins the balance in the atomic WHERE (`AND coins = :coins_now`) so a concurrent harvest yields a "balance just changed - refresh" `GameError` instead of a wrong charge, then credits the fee to the treasury.
|
||||
- **Treasury** (`game_treasury`, single row `uid="treasury-main"`, `balance`/`collected_total`/`granted_total`): every refactor fee is credited atomically (`credit_treasury`, raw conditional SQL like `conditional_update_farm`). The ledger invariant `balance == collected_total - granted_total` is fuzz-verified.
|
||||
- **Community grant** (`POST /game/grant`, `store.claim_grant`): once per ISO week (`last_grant_week`), an **active low-wealth, low-prestige** farm (coins < `GRANT_WEALTH_CEILING` 10k, `harvests_week >= GRANT_MIN_WEEK_HARVESTS` 5 - the activity gate blocks idle alts, prestige <= `GRANT_MAX_PRESTIGE` 5 - so post-refactor whales with a briefly low balance never draw welfare) claims `economy.grant_amount(balance)` (capped `GRANT_CAP` 2,500). Order of operations: atomic treasury debit first (`WHERE balance >= :amount`), then the atomic farm credit+week-stamp; a losing farm update refunds the treasury. Race-verified with real OS processes: concurrent claims by one farm pay exactly once, and two farms racing an underfunded treasury never overdraw it.
|
||||
- **Fan-out:** `serialize_farm` exposes `refactor_cost`/`refactor_affordable`/`refactor_carryover_pct`/`refactor_carryover_preview` plus the owner-only `grant_*`/`treasury_balance`; the shop row and `GameFarm.js._shopHtml` show the fee and hide the button until affordable; the sidebar **Community grant** section is `_game_grant.html`/`data-grant-host`/`_grantHtml`. Devii: `game_prestige` is now in `CONFIRM_REQUIRED` (large irreversible spend) with a declared `confirm` param; `game_claim_grant` is a plain auth tool. Docs: `game-prestige` updated + `game-grant` added.
|
||||
|
||||
### Admin surface
|
||||
|
||||
`/admin/game` (`routers/admin/game.py`, `admin_game.html`, sidebar entry between Bot Monitor and AI usage) shows the current Era status and the start/end forms; `AdminGameOut` is the response schema. `devplace game market prune` and `devplace game era {status,start,end}` mirror the same actions from the CLI (`cli/game.py`).
|
||||
|
||||
@ -269,6 +269,12 @@ PERK_BY_KEY = {perk.key: perk for perk in PERKS}
|
||||
PRESTIGE_MIN_LEVEL = 10
|
||||
PRESTIGE_BONUS = 0.25
|
||||
|
||||
REFACTOR_BASE_COST = 20_000
|
||||
REFACTOR_WEALTH_PCT = 0.15
|
||||
REFACTOR_CARRYOVER_BASE = 0.10
|
||||
REFACTOR_CARRYOVER_MAX = 0.95
|
||||
LEGACY_CARRYOVER_STEP = 0.05
|
||||
|
||||
STAR_BASE = 1
|
||||
LEGACY_SPEED_STEP = 0.05
|
||||
LEGACY_MULT_STEP = 0.10
|
||||
@ -321,6 +327,15 @@ LEGACY_UPGRADES: tuple[LegacyUpgrade, ...] = (
|
||||
2,
|
||||
1.8,
|
||||
),
|
||||
LegacyUpgrade(
|
||||
"carryover",
|
||||
"Golden Parachute",
|
||||
"🪂",
|
||||
"+5% of your post-fee coins carried through each refactor per level",
|
||||
5,
|
||||
2,
|
||||
1.8,
|
||||
),
|
||||
)
|
||||
|
||||
LEGACY_BY_KEY = {up.key: up for up in LEGACY_UPGRADES}
|
||||
@ -363,6 +378,9 @@ def legacy_value_text(up: LegacyUpgrade, level: int) -> str:
|
||||
return f"+{round(LEGACY_SPEED_STEP * level * 100)}% build speed"
|
||||
if up.key == "plots":
|
||||
return f"+{level} starting plots"
|
||||
if up.key == "carryover":
|
||||
pct = round(refactor_carryover_fraction(level) * 100)
|
||||
return f"{pct}% refactor carry-over"
|
||||
grace = LEGACY_DEFENSE_GRACE * level
|
||||
loss = round(LEGACY_DEFENSE_FRACTION * level * 100)
|
||||
return f"+{grace}s grace, -{loss}% steal loss"
|
||||
@ -405,6 +423,33 @@ def prestige_multiplier(prestige: int) -> float:
|
||||
return 1 + PRESTIGE_BONUS * max(0, prestige)
|
||||
|
||||
|
||||
def refactor_cost(prestige: int, coins: int) -> int:
|
||||
scaled = REFACTOR_BASE_COST * (1 + max(0, prestige)) * prestige_multiplier(prestige)
|
||||
return round(scaled + REFACTOR_WEALTH_PCT * max(0, coins))
|
||||
|
||||
|
||||
def refactor_carryover_fraction(carryover_level: int = 0) -> float:
|
||||
return min(
|
||||
REFACTOR_CARRYOVER_MAX,
|
||||
REFACTOR_CARRYOVER_BASE + LEGACY_CARRYOVER_STEP * max(0, carryover_level),
|
||||
)
|
||||
|
||||
|
||||
def refactor_carryover(coins: int, fee: int, carryover_level: int = 0) -> int:
|
||||
remainder = max(0, coins - fee)
|
||||
return int(remainder * refactor_carryover_fraction(carryover_level))
|
||||
|
||||
|
||||
GRANT_WEALTH_CEILING = 10_000
|
||||
GRANT_MIN_WEEK_HARVESTS = 5
|
||||
GRANT_MAX_PRESTIGE = 5
|
||||
GRANT_CAP = 2_500
|
||||
|
||||
|
||||
def grant_amount(treasury_balance: int) -> int:
|
||||
return max(0, min(GRANT_CAP, treasury_balance))
|
||||
|
||||
|
||||
UNDERDOG_COIN_RATIO = 10
|
||||
UNDERDOG_DURATION_HOURS = 24
|
||||
UNDERDOG_MULTIPLIER = 1.25
|
||||
|
||||
@ -59,6 +59,7 @@ from .infrastructure import buy_infrastructure, owns_infrastructure
|
||||
from .market import market_factor_for, prune_ticks, recent_harvests
|
||||
from .mastery import upgrade_mastery
|
||||
from .quests import advance_quests, ensure_quests
|
||||
from .treasury import claim_grant, ensure_treasury, grant_status, treasury_balance
|
||||
from .serialize import (
|
||||
_daily_available,
|
||||
_plot_state,
|
||||
|
||||
@ -488,6 +488,8 @@ def upgrade_legacy(user: dict, key: str) -> dict:
|
||||
|
||||
|
||||
def prestige(user: dict) -> dict:
|
||||
from .treasury import credit_treasury
|
||||
|
||||
farm = ensure_farm(user["uid"])
|
||||
level = economy.level_for_xp(int(farm.get("xp", 0)))
|
||||
if level < economy.PRESTIGE_MIN_LEVEL:
|
||||
@ -496,6 +498,13 @@ def prestige(user: dict) -> dict:
|
||||
)
|
||||
old_prestige = _lvl(farm, "prestige")
|
||||
new_prestige = old_prestige + 1
|
||||
coins_now = int(farm.get("coins", 0))
|
||||
fee = economy.refactor_cost(old_prestige, coins_now)
|
||||
if coins_now < fee:
|
||||
raise GameError(
|
||||
f"Refactoring costs {fee} coins right now - keep farming to afford it."
|
||||
)
|
||||
carried = economy.refactor_carryover(coins_now, fee, _lvl(farm, "legacy_carryover"))
|
||||
stars_award = economy.stars_for_refactor(level, old_prestige)
|
||||
mastery_award = economy.mastery_points_awarded(old_prestige, new_prestige)
|
||||
base_plots = economy.prestige_base_plots(_lvl(farm, "legacy_plots"))
|
||||
@ -503,7 +512,7 @@ def prestige(user: dict) -> dict:
|
||||
now = _iso(now_dt)
|
||||
|
||||
set_parts = [
|
||||
"coins = :starting_coins",
|
||||
"coins = :post_coins",
|
||||
"xp = 0",
|
||||
"level = 1",
|
||||
"ci_tier = 1",
|
||||
@ -515,13 +524,14 @@ def prestige(user: dict) -> dict:
|
||||
"prestiged_at = :now",
|
||||
]
|
||||
params = {
|
||||
"starting_coins": economy.STARTING_COINS,
|
||||
"post_coins": economy.STARTING_COINS + carried,
|
||||
"base_plots": base_plots,
|
||||
"new_prestige": new_prestige,
|
||||
"stars_award": stars_award,
|
||||
"mastery_award": mastery_award,
|
||||
"now": now,
|
||||
"old_prestige": old_prestige,
|
||||
"coins_now": coins_now,
|
||||
}
|
||||
for perk in economy.PERKS:
|
||||
column = PERK_COLUMN[perk.key]
|
||||
@ -530,16 +540,19 @@ def prestige(user: dict) -> dict:
|
||||
rows = conditional_update_farm(
|
||||
farm["uid"],
|
||||
set_clause=", ".join(set_parts),
|
||||
where_clause="COALESCE(prestige, 0) = :old_prestige",
|
||||
where_clause="COALESCE(prestige, 0) = :old_prestige AND coins = :coins_now",
|
||||
params=params,
|
||||
)
|
||||
if rows == 0:
|
||||
farm = get_farm(user["uid"])
|
||||
if _lvl(farm, "prestige") != old_prestige:
|
||||
raise GameError("Your farm already refactored - refresh and try again.")
|
||||
if int(farm.get("coins", 0)) != coins_now:
|
||||
raise GameError("Your coin balance just changed - refresh and try again.")
|
||||
raise GameError(
|
||||
f"Reach level {economy.PRESTIGE_MIN_LEVEL} to refactor (prestige)."
|
||||
)
|
||||
credit_treasury(fee)
|
||||
|
||||
kept_slots = set()
|
||||
for plot in get_plots(farm["uid"]):
|
||||
@ -561,7 +574,13 @@ def prestige(user: dict) -> dict:
|
||||
for slot_index in range(base_plots):
|
||||
if slot_index not in kept_slots:
|
||||
_create_plot(farm["uid"], user["uid"], slot_index, now)
|
||||
return {"prestige": new_prestige, "stars_awarded": stars_award, "mastery_awarded": mastery_award}
|
||||
return {
|
||||
"prestige": new_prestige,
|
||||
"stars_awarded": stars_award,
|
||||
"mastery_awarded": mastery_award,
|
||||
"fee": fee,
|
||||
"carried": carried,
|
||||
}
|
||||
|
||||
|
||||
def fertilize(user: dict, slot: int) -> dict:
|
||||
|
||||
@ -137,6 +137,7 @@ def serialize_farm(
|
||||
) -> dict:
|
||||
from .actions import _auto_harvest
|
||||
from .defense import charge_upkeep
|
||||
from .treasury import grant_status, treasury_balance
|
||||
|
||||
now = now or _now()
|
||||
viewer_uid = viewer["uid"] if viewer else ""
|
||||
@ -193,6 +194,14 @@ def serialize_farm(
|
||||
)
|
||||
building_defense_level = _lvl(farm, "defense_level")
|
||||
defense_tier_info = economy.defense_tier(building_defense_level)
|
||||
coins = int(farm.get("coins", 0))
|
||||
refactor_fee = economy.refactor_cost(prestige, coins)
|
||||
carryover_level = _lvl(farm, "legacy_carryover")
|
||||
grant = (
|
||||
grant_status(farm, now)
|
||||
if is_owner
|
||||
else {"available": False, "amount": 0, "reason": ""}
|
||||
)
|
||||
return {
|
||||
"owner_username": owner.get("username", ""),
|
||||
"owner_uid": owner_uid,
|
||||
@ -240,6 +249,20 @@ def serialize_farm(
|
||||
"prestige_multiplier": economy.prestige_multiplier(prestige),
|
||||
"prestige_min_level": economy.PRESTIGE_MIN_LEVEL,
|
||||
"prestige_available": is_owner and level >= economy.PRESTIGE_MIN_LEVEL,
|
||||
"refactor_cost": refactor_fee,
|
||||
"refactor_affordable": is_owner
|
||||
and level >= economy.PRESTIGE_MIN_LEVEL
|
||||
and coins >= refactor_fee,
|
||||
"refactor_carryover_pct": round(
|
||||
economy.refactor_carryover_fraction(carryover_level) * 100
|
||||
),
|
||||
"refactor_carryover_preview": economy.refactor_carryover(
|
||||
coins, refactor_fee, carryover_level
|
||||
),
|
||||
"grant_available": grant["available"],
|
||||
"grant_amount": grant["amount"],
|
||||
"grant_reason": grant["reason"],
|
||||
"treasury_balance": treasury_balance() if is_owner else 0,
|
||||
"streak": streak,
|
||||
"daily_available": daily_available,
|
||||
"daily_reward": economy.daily_reward(streak + 1 if daily_available else streak),
|
||||
|
||||
@ -87,6 +87,7 @@ export class GameFarm {
|
||||
this._setHost("[data-analytics-host]", this._analyticsHtml(farm));
|
||||
this._setHost("[data-cosmetics-host]", this._cosmeticsHtml(farm));
|
||||
this._setHost("[data-daily-host]", this._dailyHtml(farm));
|
||||
this._setHost("[data-grant-host]", this._grantHtml(farm));
|
||||
this._setHost("[data-quest-host]", this._questsHtml(farm));
|
||||
}
|
||||
this._tick();
|
||||
@ -128,12 +129,17 @@ export class GameFarm {
|
||||
const ciText = farm.ci_next_tier
|
||||
? `Upgrade to ${farm.ci_next_label} for faster builds`
|
||||
: "CI is at the top tier.";
|
||||
const prestigeBtn = farm.prestige_available
|
||||
? `<form method="post" action="/game/prestige" data-game-action="prestige"><button type="submit" class="btn btn-sm btn-primary" data-confirm="Refactor resets coins, level, CI, extra plots, and perks for a permanent +25% coin bonus. Continue?">Refactor</button></form>`
|
||||
const prestigeBtn = farm.refactor_affordable
|
||||
? `<form method="post" action="/game/prestige" data-game-action="prestige"><button type="submit" class="btn btn-sm btn-primary" data-confirm="Refactoring costs ${farm.refactor_cost}c and resets coins, level, CI, extra plots, and perks for a permanent +25% coin bonus. You carry over ${farm.refactor_carryover_preview}c and keep your Stars and Legacy upgrades. Continue?">Refactor (${farm.refactor_cost}c)</button></form>`
|
||||
: "";
|
||||
const prestigeText = farm.prestige_available
|
||||
? "Reset your farm for a permanent +25% coin bonus."
|
||||
: `Reach level ${farm.prestige_min_level} to refactor.`;
|
||||
let prestigeText;
|
||||
if (!farm.prestige_available) {
|
||||
prestigeText = `Reach level ${farm.prestige_min_level} to refactor.`;
|
||||
} else if (farm.refactor_affordable) {
|
||||
prestigeText = `Pay ${farm.refactor_cost}c to reset your farm for a permanent +25% coin bonus. You keep ${farm.refactor_carryover_pct}% of what remains (${farm.refactor_carryover_preview}c) plus your Stars and Legacy upgrades.`;
|
||||
} else {
|
||||
prestigeText = `Refactoring costs ${farm.refactor_cost}c right now - keep farming to afford it.`;
|
||||
}
|
||||
return (
|
||||
`<div class="shop-row"><div class="shop-info"><strong>CI upgrade</strong><span>${ciText}</span></div>${ciUpgrade}</div>` +
|
||||
`<div class="shop-row"><div class="shop-info"><strong>Refactor (prestige ${farm.prestige})</strong><span>${prestigeText}</span></div>${prestigeBtn}</div>`
|
||||
@ -226,6 +232,13 @@ export class GameFarm {
|
||||
.join("");
|
||||
}
|
||||
|
||||
_grantHtml(farm) {
|
||||
const action = farm.grant_available
|
||||
? `<form method="post" action="/game/grant" data-game-action="grant"><button type="submit" class="btn btn-sm btn-primary">Claim +${farm.grant_amount}c</button></form>`
|
||||
: `<span class="daily-claimed">${farm.grant_reason}</span>`;
|
||||
return `<p class="daily-streak">Treasury: <strong>${farm.treasury_balance}</strong>c collected from refactor fees</p>${action}`;
|
||||
}
|
||||
|
||||
_dailyHtml(farm) {
|
||||
const action = farm.daily_available
|
||||
? `<form method="post" action="/game/daily" data-game-action="daily"><button type="submit" class="btn btn-sm btn-primary">Claim +${farm.daily_reward}c</button></form>`
|
||||
|
||||
@ -12,11 +12,11 @@
|
||||
<div class="shop-row">
|
||||
<div class="shop-info">
|
||||
<strong>Refactor (prestige {{ farm.prestige }})</strong>
|
||||
<span>{% if farm.prestige_available %}Reset your farm for a permanent +25% coin bonus and earn Stars to spend on Legacy upgrades.{% else %}Reach level {{ farm.prestige_min_level }} to refactor.{% endif %}</span>
|
||||
<span>{% if not farm.prestige_available %}Reach level {{ farm.prestige_min_level }} to refactor.{% elif farm.refactor_affordable %}Pay {{ farm.refactor_cost }}c to reset your farm for a permanent +25% coin bonus. You keep {{ farm.refactor_carryover_pct }}% of what remains ({{ farm.refactor_carryover_preview }}c) plus your Stars and Legacy upgrades.{% else %}Refactoring costs {{ farm.refactor_cost }}c right now - keep farming to afford it.{% endif %}</span>
|
||||
</div>
|
||||
{% if farm.prestige_available %}
|
||||
{% if farm.refactor_affordable %}
|
||||
<form method="post" action="/game/prestige" data-game-action="prestige">
|
||||
<button type="submit" class="btn btn-sm btn-primary" data-confirm="Refactor resets coins, level, CI, extra plots, and perks for a permanent +25% coin bonus. You keep your Stars and Legacy upgrades and earn more Stars. Continue?">Refactor</button>
|
||||
<button type="submit" class="btn btn-sm btn-primary" data-confirm="Refactoring costs {{ farm.refactor_cost }}c and resets coins, level, CI, extra plots, and perks for a permanent +25% coin bonus. You carry over {{ farm.refactor_carryover_preview }}c and keep your Stars and Legacy upgrades. Continue?">Refactor ({{ farm.refactor_cost }}c)</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -195,13 +195,26 @@ multiplies with the Optimizer perk. Refactoring is the long-term progression and
|
||||
largest factor in the leaderboard. The state reports `prestige`, `prestige_multiplier`, and
|
||||
`prestige_available`.
|
||||
|
||||
Refactoring is not free: it costs a **coin fee** that grows with both your prestige count and your
|
||||
current coin balance (a wealth tax), so each refactor takes longer to afford than the last. The
|
||||
live state reports the exact `refactor_cost` and whether you can pay it (`refactor_affordable`).
|
||||
After the fee, **10%** of your remaining coins carry over into the new run - more with the Golden
|
||||
Parachute Legacy upgrade (up to 35%), previewed as `refactor_carryover_preview`.
|
||||
|
||||
Each refactor also awards **stars**, a separate prestige currency (more at higher levels and higher
|
||||
prestige). Stars are never reset and are spent on permanent **Legacy upgrades**.
|
||||
|
||||
## Community treasury and weekly grant
|
||||
|
||||
Every refactor fee flows into a shared **community treasury**. Once per week, an active farm that
|
||||
is still building up - at least five harvests this week, fewer than 10,000 coins, and at most
|
||||
prestige 5 - can claim a grant of up to 2,500 coins from it with `POST /game/grant`. The state
|
||||
reports `grant_available`, `grant_amount`, `grant_reason`, and the current `treasury_balance`.
|
||||
|
||||
## Legacy upgrades
|
||||
|
||||
Stars buy Legacy upgrades, permanent boosts that persist through every future refactor. There are
|
||||
five:
|
||||
six:
|
||||
|
||||
| Legacy upgrade | Effect | Max level |
|
||||
|----------------|--------|:---------:|
|
||||
@ -210,6 +223,7 @@ five:
|
||||
| 🏎️ Bare-Metal | +5% base build speed per level | 8 |
|
||||
| 🗂️ Monorepo | +1 starting plot after each refactor per level | 4 |
|
||||
| 🛡️ Branch Protection | +30s steal grace and -5% steal loss per level | 5 |
|
||||
| 🪂 Golden Parachute | +5% refactor coin carry-over per level | 5 |
|
||||
|
||||
The live state reports your `stars` balance and each legacy upgrade's current level and the exact
|
||||
star cost of its next level.
|
||||
@ -273,7 +287,8 @@ your **API key** in an `X-API-KEY` header (or `Authorization: Bearer`). Your key
|
||||
| `POST` | `/game/perk` | Upgrade `perk` (`yield`, `growth`, `discount`, `xp`). |
|
||||
| `POST` | `/game/daily` | Claim the daily bonus. |
|
||||
| `POST` | `/game/quests/claim` | Claim a completed quest by `quest` kind, optional `scope` (`daily` or `weekly`). |
|
||||
| `POST` | `/game/prestige` | Refactor at level 10 or above. |
|
||||
| `POST` | `/game/prestige` | Refactor at level 10 or above; costs the current `refactor_cost` in coins. |
|
||||
| `POST` | `/game/grant` | Claim the weekly community grant from the treasury. |
|
||||
| `POST` | `/game/legacy` | Buy a legacy upgrade (`key`) with stars. |
|
||||
| `POST` | `/game/mastery` | Buy a Mastery upgrade (`key`) with Mastery points. |
|
||||
| `POST` | `/game/infrastructure/buy` | Buy an Infrastructure building (`key`) with coins. |
|
||||
|
||||
@ -100,6 +100,11 @@
|
||||
<div data-daily-host>{% include "_game_daily.html" %}</div>
|
||||
</section>
|
||||
|
||||
<section class="game-grant">
|
||||
<h2 class="game-section-title">Community grant</h2>
|
||||
<div data-grant-host>{% include "_game_grant.html" %}</div>
|
||||
</section>
|
||||
|
||||
<section class="game-quests">
|
||||
<h2 class="game-section-title">Quests & contracts</h2>
|
||||
<ul class="quest-list" data-quest-host>{% include "_game_quests.html" %}</ul>
|
||||
|
||||
@ -6,7 +6,7 @@ from datetime import datetime, timezone, timedelta
|
||||
import requests
|
||||
|
||||
from devplacepy.database import get_table, refresh_snapshot
|
||||
from devplacepy.services.game import store
|
||||
from devplacepy.services.game import economy, store
|
||||
from tests.conftest import BASE_URL
|
||||
|
||||
JSON = {"Accept": "application/json"}
|
||||
@ -337,3 +337,91 @@ def test_steal_cooldown_blocks_second_raid(app_server, seeded_db):
|
||||
f"{BASE_URL}/game/farm/{owner}/steal", data={"slot": 0}, headers=JSON
|
||||
)
|
||||
assert second.status_code == 400
|
||||
|
||||
|
||||
def test_prestige_insufficient_coins_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100)
|
||||
_set_farm(name, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
r = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
|
||||
assert r.status_code == 400
|
||||
assert "costs" in r.json()["error"]["message"]
|
||||
|
||||
|
||||
def test_prestige_charges_fee_and_carries_over(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100000)
|
||||
_set_farm(name, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
fee = economy.refactor_cost(0, 100000)
|
||||
carried = economy.refactor_carryover(100000, fee, 0)
|
||||
r = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
|
||||
assert r.status_code == 200
|
||||
farm = r.json()["farm"]
|
||||
assert farm["prestige"] == 1
|
||||
assert farm["coins"] == economy.STARTING_COINS + carried
|
||||
refresh_snapshot()
|
||||
assert store.treasury_balance() >= fee
|
||||
|
||||
|
||||
def test_state_exposes_refactor_and_grant_fields(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name)
|
||||
r = session.get(f"{BASE_URL}/game/state", headers=JSON)
|
||||
assert r.status_code == 200
|
||||
farm = r.json()["farm"]
|
||||
for key in (
|
||||
"refactor_cost",
|
||||
"refactor_affordable",
|
||||
"refactor_carryover_pct",
|
||||
"refactor_carryover_preview",
|
||||
"grant_available",
|
||||
"grant_amount",
|
||||
"grant_reason",
|
||||
"treasury_balance",
|
||||
):
|
||||
assert key in farm
|
||||
assert farm["refactor_cost"] == economy.refactor_cost(0, farm["coins"])
|
||||
|
||||
|
||||
def test_grant_requires_auth(app_server, seeded_db):
|
||||
r = requests.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert r.status_code in (401, 303)
|
||||
|
||||
|
||||
def test_grant_claim_flow(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=100)
|
||||
refresh_snapshot()
|
||||
store.ensure_treasury()
|
||||
get_table("game_treasury").update(
|
||||
{"uid": "treasury-main", "balance": 100000}, ["uid"]
|
||||
)
|
||||
week = store._iso_week(store._now())
|
||||
_set_farm(
|
||||
name,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
harvests_week_start=week,
|
||||
)
|
||||
r = session.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["farm"]["coins"] == 100 + economy.GRANT_CAP
|
||||
second = session.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert second.status_code == 400
|
||||
|
||||
|
||||
def test_grant_ineligible_when_rich_returns_400(app_server, seeded_db):
|
||||
session, name = _signup()
|
||||
_reset_farm(name, coins=economy.GRANT_WEALTH_CEILING + 1)
|
||||
refresh_snapshot()
|
||||
store.ensure_treasury()
|
||||
get_table("game_treasury").update(
|
||||
{"uid": "treasury-main", "balance": 100000}, ["uid"]
|
||||
)
|
||||
week = store._iso_week(store._now())
|
||||
_set_farm(
|
||||
name,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
harvests_week_start=week,
|
||||
)
|
||||
r = session.post(f"{BASE_URL}/game/grant", headers=JSON)
|
||||
assert r.status_code == 400
|
||||
|
||||
@ -54,7 +54,7 @@ def test_admin_sees_revoke_button(alice, bob):
|
||||
admin_page, _ = alice
|
||||
member_page, _ = bob
|
||||
admin_page.goto(f"{BASE_URL}/profile/bob_test?tab=awards", wait_until="domcontentloaded")
|
||||
expect(admin_page.locator(AWARD_REVOKE)).to_be_visible()
|
||||
expect(admin_page.locator(AWARD_REVOKE).first).to_be_visible()
|
||||
member_page.goto(f"{BASE_URL}/profile/bob_test?tab=awards", wait_until="domcontentloaded")
|
||||
expect(member_page.locator(AWARD_REVOKE)).to_have_count(0)
|
||||
|
||||
|
||||
@ -97,7 +97,7 @@ def test_member_sees_only_quota_percentage(bob):
|
||||
assert (
|
||||
page.locator(".ai-quota-only .ai-usage-title", has_text="AI quota").count() == 1
|
||||
)
|
||||
assert "%" in page.locator(".ai-quota-only .ai-quota-pct").inner_text()
|
||||
assert "%" in page.locator(".ai-quota-only .ai-quota-pct").first.inner_text()
|
||||
assert page.locator("[data-ai-usage]").count() == 0
|
||||
|
||||
|
||||
|
||||
@ -324,7 +324,7 @@ def test_landing_authenticated_shows_dashboard(page, app_server):
|
||||
assert page.url.rstrip("/") == BASE_URL.rstrip("/")
|
||||
assert page.is_visible("text=Welcome back")
|
||||
assert page.is_visible("text=home_user")
|
||||
feed_cta = page.locator("a.landing-cta:has-text('Go to your feed')")
|
||||
feed_cta = page.locator("a.dashboard-btn-primary:has-text('New Post')")
|
||||
assert feed_cta.is_visible()
|
||||
assert feed_cta.get_attribute("href") == "/feed"
|
||||
assert not page.is_visible("text=Join DevPlace Free")
|
||||
|
||||
@ -232,6 +232,8 @@ def test_prestige_resets_farm(alice):
|
||||
|
||||
reset_farm("alice_test")
|
||||
set_farm_xp("alice_test", economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
fee = economy.refactor_cost(0, 100000)
|
||||
carried = economy.refactor_carryover(100000, fee, 0)
|
||||
open_game(page)
|
||||
prestige = page.locator("form[data-game-action='prestige'] button")
|
||||
prestige.wait_for(state="visible")
|
||||
@ -240,10 +242,49 @@ def test_prestige_resets_farm(alice):
|
||||
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")
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text(
|
||||
str(economy.STARTING_COINS + carried)
|
||||
)
|
||||
assert page.locator("form[data-game-action='prestige']").count() == 0
|
||||
|
||||
|
||||
def test_prestige_unaffordable_shows_cost_without_button(alice):
|
||||
page, _ = alice
|
||||
from devplacepy.services.game import economy
|
||||
|
||||
reset_farm("alice_test", coins=100)
|
||||
set_farm_xp("alice_test", economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
open_game(page)
|
||||
assert page.locator("form[data-game-action='prestige']").count() == 0
|
||||
assert page.is_visible("text=keep farming to afford it")
|
||||
|
||||
|
||||
def test_grant_section_claim(alice):
|
||||
page, _ = alice
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.services.game import economy, store
|
||||
from devplacepy.services.game.store import _iso_week, _now
|
||||
|
||||
reset_farm("alice_test", coins=100)
|
||||
store.ensure_treasury()
|
||||
get_table("game_treasury").update(
|
||||
{"uid": "treasury-main", "balance": 100000}, ["uid"]
|
||||
)
|
||||
set_farm(
|
||||
"alice_test",
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
harvests_week_start=_iso_week(_now()),
|
||||
)
|
||||
open_game(page)
|
||||
claim = page.locator("form[data-game-action='grant'] button")
|
||||
claim.wait_for(state="visible")
|
||||
claim.click()
|
||||
expect(page.locator("[data-hud-coins]")).to_have_text(
|
||||
str(100 + economy.GRANT_CAP)
|
||||
)
|
||||
assert page.locator("form[data-game-action='grant']").count() == 0
|
||||
|
||||
|
||||
def test_leaderboard_panel_populates(alice):
|
||||
page, _ = alice
|
||||
reset_farm("alice_test")
|
||||
|
||||
@ -399,3 +399,67 @@ def test_weekly_contract_deterministic():
|
||||
assert first == second
|
||||
assert first["kind"] in economy.QUEST_DEFS
|
||||
assert first["reward_stars"] > 0
|
||||
|
||||
|
||||
# --- refactor fee, carry-over, and grants -------------------------------------------
|
||||
|
||||
|
||||
def test_refactor_cost_monotonic_in_prestige():
|
||||
previous = 0
|
||||
for prestige in range(0, 200):
|
||||
cost = economy.refactor_cost(prestige, 0)
|
||||
assert cost > previous
|
||||
previous = cost
|
||||
|
||||
|
||||
def test_refactor_cost_monotonic_in_wealth():
|
||||
previous = -1
|
||||
for coins in range(0, 2_000_000, 10_000):
|
||||
cost = economy.refactor_cost(0, coins)
|
||||
assert cost >= previous
|
||||
previous = cost
|
||||
|
||||
|
||||
def test_refactor_cost_floor_is_base():
|
||||
assert economy.refactor_cost(0, 0) == economy.REFACTOR_BASE_COST
|
||||
assert economy.refactor_cost(-5, -100) == economy.REFACTOR_BASE_COST
|
||||
|
||||
|
||||
def test_refactor_cost_includes_wealth_tax():
|
||||
coins = 100_000
|
||||
expected = round(
|
||||
economy.REFACTOR_BASE_COST + economy.REFACTOR_WEALTH_PCT * coins
|
||||
)
|
||||
assert economy.refactor_cost(0, coins) == expected
|
||||
|
||||
|
||||
def test_refactor_carryover_fraction_bounds():
|
||||
for level in range(0, 50):
|
||||
fraction = economy.refactor_carryover_fraction(level)
|
||||
assert economy.REFACTOR_CARRYOVER_BASE <= fraction <= economy.REFACTOR_CARRYOVER_MAX
|
||||
|
||||
|
||||
def test_refactor_carryover_never_exceeds_remainder():
|
||||
for coins in range(0, 500_000, 7_777):
|
||||
for level in (0, 3, 5):
|
||||
fee = economy.refactor_cost(0, coins)
|
||||
carried = economy.refactor_carryover(coins, fee, level)
|
||||
assert 0 <= carried <= max(0, coins - fee)
|
||||
|
||||
|
||||
def test_refactor_carryover_zero_when_unaffordable():
|
||||
assert economy.refactor_carryover(100, 20_000, 5) == 0
|
||||
|
||||
|
||||
def test_grant_amount_capped_and_bounded():
|
||||
assert economy.grant_amount(0) == 0
|
||||
assert economy.grant_amount(-10) == 0
|
||||
assert economy.grant_amount(100) == 100
|
||||
assert economy.grant_amount(10**9) == economy.GRANT_CAP
|
||||
|
||||
|
||||
def test_legacy_carryover_upgrade_registered():
|
||||
upgrade = economy.legacy_for("carryover")
|
||||
assert upgrade is not None
|
||||
assert upgrade.max_level == 5
|
||||
assert "carry-over" in economy.legacy_value_text(upgrade, 2)
|
||||
|
||||
@ -45,6 +45,7 @@ def _reset(username, coins=100000):
|
||||
get_table("game_steals").delete(owner_uid=user["uid"])
|
||||
get_table("game_cosmetics").delete(user_uid=user["uid"])
|
||||
get_table("game_market_ticks").delete()
|
||||
get_table("game_treasury").delete()
|
||||
_clear_game_caches()
|
||||
farm = store.ensure_farm(user["uid"])
|
||||
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
||||
@ -493,10 +494,15 @@ def test_prestige_resets_farm_and_increments(local_db):
|
||||
ci_tier=3,
|
||||
)
|
||||
store.buy_plot(user)
|
||||
coins_before = _coins(user)
|
||||
fee = economy.refactor_cost(0, coins_before)
|
||||
carried = economy.refactor_carryover(coins_before, fee, 0)
|
||||
result = store.prestige(user)
|
||||
assert result["prestige"] == 1
|
||||
assert result["fee"] == fee
|
||||
assert result["carried"] == carried
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert farm["coins"] == economy.STARTING_COINS
|
||||
assert farm["coins"] == economy.STARTING_COINS + carried
|
||||
assert farm["xp"] == 0
|
||||
assert farm["ci_tier"] == 1
|
||||
assert farm["perk_growth"] == 0
|
||||
@ -504,6 +510,45 @@ def test_prestige_resets_farm_and_increments(local_db):
|
||||
assert len(store.get_plots(farm["uid"])) == economy.STARTING_PLOTS
|
||||
|
||||
|
||||
def test_prestige_insufficient_coins_blocked(local_db):
|
||||
user = _reset("unit_a", coins=100)
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
with pytest.raises(GameError):
|
||||
store.prestige(user)
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert int(farm["prestige"] or 0) == 0
|
||||
assert int(farm["coins"]) == 100
|
||||
|
||||
|
||||
def test_prestige_fee_funds_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL))
|
||||
fee = economy.refactor_cost(0, _coins(user))
|
||||
store.prestige(user)
|
||||
assert store.treasury_balance() == fee
|
||||
|
||||
|
||||
def test_prestige_carryover_scales_with_legacy(local_db):
|
||||
user = _reset("unit_a")
|
||||
_set(
|
||||
user,
|
||||
xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL),
|
||||
legacy_carryover=5,
|
||||
)
|
||||
coins_before = _coins(user)
|
||||
fee = economy.refactor_cost(0, coins_before)
|
||||
carried = economy.refactor_carryover(coins_before, fee, 5)
|
||||
assert carried > economy.refactor_carryover(coins_before, fee, 0)
|
||||
store.prestige(user)
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert farm["coins"] == economy.STARTING_COINS + carried
|
||||
|
||||
|
||||
def test_prestige_fee_rises_with_prestige_and_wealth(local_db):
|
||||
assert economy.refactor_cost(1, 0) > economy.refactor_cost(0, 0)
|
||||
assert economy.refactor_cost(0, 1_000_000) > economy.refactor_cost(0, 0)
|
||||
|
||||
|
||||
# --- legacy upgrades ---------------------------------------------------------
|
||||
|
||||
|
||||
@ -851,7 +896,8 @@ def test_farm_analytics_tracks_lifetime_totals_once_unlocked(local_db):
|
||||
|
||||
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)
|
||||
coins = economy.refactor_cost(49, 0) * 2
|
||||
_set(user, xp=economy.xp_threshold(economy.PRESTIGE_MIN_LEVEL), prestige=49, coins=coins)
|
||||
result = store.prestige(user)
|
||||
assert result["mastery_awarded"] == 1
|
||||
farm = store.get_farm(user["uid"])
|
||||
@ -946,3 +992,87 @@ def test_weekly_contract_claim_pays_stars_and_boost(local_db):
|
||||
farm = store.get_farm(user["uid"])
|
||||
assert int(farm["stars"]) == result["reward_stars"]
|
||||
assert farm.get("contract_boost_until")
|
||||
|
||||
|
||||
# --- treasury and community grant --------------------------------------------
|
||||
|
||||
|
||||
def _seed_treasury(amount):
|
||||
store.ensure_treasury()
|
||||
get_table("game_treasury").update(
|
||||
{"uid": "treasury-main", "balance": amount}, ["uid"]
|
||||
)
|
||||
|
||||
|
||||
def _make_grant_eligible(user):
|
||||
_set(
|
||||
user,
|
||||
coins=100,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
harvests_week_start=store._iso_week(store._now()),
|
||||
)
|
||||
|
||||
|
||||
def test_claim_grant_pays_from_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_make_grant_eligible(user)
|
||||
result = store.claim_grant(user)
|
||||
assert result["amount"] == economy.GRANT_CAP
|
||||
assert _coins(user) == 100 + economy.GRANT_CAP
|
||||
assert store.treasury_balance() == 100_000 - economy.GRANT_CAP
|
||||
|
||||
|
||||
def test_claim_grant_once_per_week(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_make_grant_eligible(user)
|
||||
store.claim_grant(user)
|
||||
_set(user, coins=100)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_requires_activity(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_set(user, coins=100, harvests_week=economy.GRANT_MIN_WEEK_HARVESTS - 1)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_wealth_ceiling(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_set(
|
||||
user,
|
||||
coins=economy.GRANT_WEALTH_CEILING,
|
||||
harvests_week=economy.GRANT_MIN_WEEK_HARVESTS,
|
||||
)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_prestige_cap(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(100_000)
|
||||
_make_grant_eligible(user)
|
||||
_set(user, prestige=economy.GRANT_MAX_PRESTIGE + 1)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_empty_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_make_grant_eligible(user)
|
||||
with pytest.raises(GameError):
|
||||
store.claim_grant(user)
|
||||
|
||||
|
||||
def test_claim_grant_partial_treasury(local_db):
|
||||
user = _reset("unit_a")
|
||||
_seed_treasury(300)
|
||||
_make_grant_eligible(user)
|
||||
result = store.claim_grant(user)
|
||||
assert result["amount"] == 300
|
||||
assert store.treasury_balance() == 0
|
||||
|
||||
Loading…
Reference in New Issue
Block a user