This commit is contained in:
2026-07-23 01:14:10 +02:00
parent 64c3983c9f
commit 582e37d176
24 changed files with 549 additions and 36 deletions
+13
View File
@@ -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", ""),
+15 -3
View File
@@ -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}},
+2 -2
View File
@@ -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`).
+6
View File
@@ -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)
+8
View File
@@ -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 = {
+13 -3
View File
@@ -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`).
+45
View File
@@ -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,
+23 -4
View File
@@ -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),
+18 -5
View File
@@ -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>`
+3 -3
View File
@@ -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>
+17 -2
View File
@@ -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. |
+5
View File
@@ -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 &amp; contracts</h2>
<ul class="quest-list" data-quest-host>{% include "_game_quests.html" %}</ul>