forked from retoor/devplacepy
pdate
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user