diff --git a/CLAUDE.md b/CLAUDE.md index 69fa5555..ca0fb3b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,7 @@ devplace isslop analyze # run a AI usage analysis from the terminal (repo devplace isslop prune # delete expired AI usage analysis job rows (analyses + reports persist) devplace isslop clear # delete every AI usage analysis, its report and job rows devplace game market prune # delete Code Farm market tick buckets older than the tracking window +devplace game steals prune # delete Code Farm raid records older than the raid-efficiency window devplace game era status # show the current Code Farm Era devplace game era start [--days N] # start a Code Farm Era (default 28 days) devplace game era end # end the running Code Farm Era (ranks, awards Stars, records results) @@ -136,7 +137,7 @@ Nested `CLAUDE.md` files (loaded automatically by Claude Code only when a file i | `devplacepy/services/bot/CLAUDE.md` | `BotsService` fleet | | `devplacepy/services/dbapi/CLAUDE.md` | `/dbapi` primary-admin-only read-only database API | | `devplacepy/services/pubsub/CLAUDE.md` | Database-free pub/sub bus | -| `devplacepy/services/game/CLAUDE.md` | Code Farm idle game | +| `devplacepy/services/game/CLAUDE.md` | Code Farm idle game (economy invariants, raids, the one-pure-function rule) | | `devplacepy/services/CLAUDE.md` | Background task queue, AI correction/modifier, presence, live view relay, `BaseService`/`ServiceManager` | | `devplacepy/database/CLAUDE.md` | Dataset rules, indexing conventions, soft delete, tables, site settings | | `devplacepy/utils/CLAUDE.md` | Notifications and gamification (XP/levels/badges/leaderboard) | diff --git a/README.md b/README.md index 4e302ffe..c2619f0c 100644 --- a/README.md +++ b/README.md @@ -125,21 +125,21 @@ 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. 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. +- **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 60%) 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 grant from it once per week - the balance is divided between everyone currently eligible rather than paid first-come-first-served, capped at 2,500 coins and suppressed below 250. 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. -- **Market Saturation.** The last 48 hours of league-wide harvests of each crop are tracked and converted into grow-time-normalized supply, so fast and slow crops saturate on the same real-terms scale; when a crop is over-farmed its payout drops in steps (down to 40%), while the four starter crops pay a boost (up to +15%) whenever the high-tier market is saturated and they are not - a crop is either penalized or boosted, never both. Printing one crop nonstop is throttled, planting what the market is short on is rewarded. The shop shows a live "Saturated" / "Boosted" label per crop. -- **Infrastructure.** Permanent, expensive, prestige-gated buildings and coin sinks: **Private Registry** (faster Rust/Compiler/Kernel builds), **Canary Deployments** (a chance to double or only refund a harvest), and **Observability Suite** (fixes the raid payout floor at 30% of a build's value). -- **Defense.** An upgradeable building that reduces raid losses and adds steal grace - but costs an ongoing daily coin upkeep (proportional to your coin balance, so it scales with wealth); if unpaid, the tier decays automatically. +- **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 - scaled by your own prestige and Tech Debt Payoff multiplier, so the cooperative loop stays worth doing at every stage - 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 raid pays the thief a share of the build's coin value and the **owner keeps and can still harvest the remainder** - a raid redistributes value rather than destroying it. The thief earns the **Cat Burglar** badge; the victim gets the **Robbed** badge and a notification naming the raider, the crop, and the exact amount taken. You can raid any given neighbour only **once per hour**, and any farm can absorb at most **3 raids per day**, so an inactive player can never be stripped by an unlimited queue of raiders. 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. +- **Market Saturation.** The last 48 hours of league-wide harvests of each crop are tracked and converted into grow-time-normalized supply, so fast and slow crops saturate on the same real-terms scale; supply is measured per active farm so a busy server is not permanently floored by a few heavy players; when a crop is over-farmed its payout drops in steps (down to 40%), while the four starter crops pay a boost (up to +15%) whenever the high-tier market is saturated and they are not - a crop is either penalized or boosted, never both. Printing one crop nonstop is throttled, planting what the market is short on is rewarded. The shop shows a live "Saturated" / "Boosted" label per crop. +- **Infrastructure.** Permanent, expensive, prestige-gated buildings and coin sinks: **Private Registry** (faster Rust/Compiler/Kernel builds), **Canary Deployments** (a chance to double or only refund a harvest), and **Observability Suite** (caps what any raider can take from you at 20% of a build's value). +- **Defense.** An upgradeable building that multiplicatively reduces raid losses and adds steal grace - but costs an ongoing daily coin upkeep (proportional to your coin balance, so it scales with wealth). If you cannot pay, only what you can afford is taken and the tier decays by one level - your balance is never emptied - and you are notified. You can also step down a tier deliberately to leave the commitment. - **Cosmetics.** Purely cosmetic titles and plot skins, bought with coins - zero gameplay effect, pure status. An equipped title shows next to your name on the leaderboard. -- **Mastery (endgame beyond prestige).** From prestige 50 onward, every 10 more prestige earns a permanent Mastery point (spendable, and the milestone itself never re-locks). Mastery upgrades open new gameplay instead of bigger numbers: **Continuous Delivery** (auto-replant after harvest), **Farm Analytics** (lifetime stats on your HUD), and **Legacy Contracts** (a weekly long-term contract slot paying Stars and a temporary coin boost). Reaching Mastery also unlocks three new high-tier crop families (Distributed System, ML Pipeline, Security Fortress - the last one immune to raids). -- **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. +- **Mastery (endgame beyond prestige).** From prestige 50 onward, every 5 more prestige earns a permanent Mastery point (spendable, and the milestone itself never re-locks). Mastery upgrades open new gameplay instead of bigger numbers: **Continuous Delivery** (auto-replant after harvest), **Farm Analytics** (lifetime stats on your HUD), and **Legacy Contracts** (a weekly long-term contract slot paying Stars and a temporary coin boost). Reaching Mastery also unlocks three new high-tier crop families (Distributed System, ML Pipeline, Security Fortress - the last one immune to raids). +- **Leaderboards.** Several boards, selectable from the game page: **Overall score** (a composite weighing refactor/prestige count, XP, lifetime harvests, a capped coin contribution, CI tier, plots, perks, and streak - the cap keeps it a measure of what you built rather than what you hoard), **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 an 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_fertilize`, `game_daily`, `game_claim_quest`, `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** and the full player guide at `/docs/code-farm.html`. +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_fertilize`, `game_daily`, `game_claim_quest`, `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`, `game_downgrade_defense`). See the API reference group **Code Farm** and the full player guide at `/docs/code-farm.html`. ## Engagement diff --git a/devplacepy/routers/CLAUDE.md b/devplacepy/routers/CLAUDE.md index b397f506..0c4862ca 100644 --- a/devplacepy/routers/CLAUDE.md +++ b/devplacepy/routers/CLAUDE.md @@ -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,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` | +| `/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,defense/downgrade,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` | diff --git a/devplacepy/services/game/CLAUDE.md b/devplacepy/services/game/CLAUDE.md index 328e2368..0ca9d50c 100644 --- a/devplacepy/services/game/CLAUDE.md +++ b/devplacepy/services/game/CLAUDE.md @@ -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?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}`. Customer-facing documentation is the `docs_api.py` **Code Farm** group (`docs_api/groups/game.py`, enum `options` on every key param) and the prose guide `templates/docs/code-farm.html` (`/docs/code-farm.html` - the complete rules, every exact formula and catalog table, the full farm/plot field reference, the JSON error shape, and a stdlib automation client); keep BOTH in lockstep with `economy.py` whenever a constant, formula, catalog entry, field, or endpoint changes. +`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?board=`, `POST /game/{plant,harvest,buy-plot,upgrade,fertilize,daily,grant,perk,prestige,legacy,mastery,quests/claim}`, `POST /game/{defense/upgrade,defense/downgrade,infrastructure/buy,cosmetics/buy,cosmetics/equip}`, plus social `GET /game/farm/{username}` and `POST /game/farm/{username}/{water,steal}`. Customer-facing documentation is the `docs_api.py` **Code Farm** group (`docs_api/groups/game.py`, enum `options` on every key param) and the prose guide `templates/docs/code-farm.html` (`/docs/code-farm.html` - the complete rules, every exact formula and catalog table, the full farm/plot field reference, the JSON error shape, and a stdlib automation client); keep BOTH in lockstep with `economy.py` whenever a constant, formula, catalog entry, field, or endpoint changes. 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. @@ -32,7 +32,7 @@ Devii plays via the `game_*` `http` actions in `actions/catalog.py` (state/leade ## Stealing (competitive loop, backwards compatible) -`store.steal(thief, owner, slot)` mirrors `water`: it requires the owner plot to be `ready` AND past the protection window `economy.effective_steal_grace(owner_defense_level)` (base `STEAL_GRACE_SECONDS` 60s, +30s per owner Branch Protection level) measured from `ready_at`, AND that the thief is **off cooldown for this victim** (`steal_cooldown_remaining(thief, owner, now) == 0`, i.e. no row in `game_steals` for the pair within `STEAL_COOLDOWN_SECONDS` = 3600 - you can raid a given neighbour only once per hour); it clears the plot exactly like a harvest (owner gets nothing), credits the **thief's** farm `economy.steal_reward_coins` (the owner's yield/prestige/legacy-multiplier realized value times `effective_steal_fraction(owner_defense_level)`, base `STEAL_FRACTION` 0.5, -5% per defense level, floored at 0.1) and **coins only** - no XP, no `total_harvests`, so the leaderboard stays earned by real farming - then inserts a `game_steals` row stamping the cooldown. The route `POST /game/farm/{username}/steal` (reusing `GameSlotForm`) then `track_action`s both sides, fires `create_notification(owner, "harvest_stolen", "Someone raided your Code Farm...", thief_uid, "/game")` (the message never names the thief; `related_uid` is internal), and `await notify_farm`s **both** the owner and the thief usernames so both farms refresh live. The new `harvest_stolen` notification type rides the existing in-app relay (live toast, no new wiring). No DB migration: grace + payout are computed from existing `ready_at`/perk/legacy columns, the `game_steals` table is created by the ensure-block (absent rows -> cooldown 0 -> first steal always allowed), and the new `GamePlotOut.can_steal`/`steal_coins`/`steal_cooldown_seconds`/`steal_reason` + `GameFarmOut.steal_cooldown_seconds` + `GameFarmViewOut.stole_coins` schema fields default safe. The per-pair cooldown is computed **once per farm** in `serialize_farm` (when the viewer is not the owner) and threaded into every `serialize_plot` as `steal_locked_until`, so `can_steal` is false and `steal_reason` is `"cooldown"`/`"protected"` accordingly. **Display must equal enforcement:** `serialize_farm` also threads the owner's Defense building into every `serialize_plot` (`steal_grace_bonus` = the tier's `grace_bonus`, `steal_floor` = the tier's `steal_fraction_floor` raised to `OBSERVABILITY_STEAL_FLOOR` when the owner owns Observability), so the serialized `can_steal`/`steal_coins` match exactly what `store/actions.py::steal()` will enforce and pay - a client acting on `can_steal=true` can never get a "still protected" 400, and `steal_coins` is the exact payout. Never let `serialize_plot` and `steal()` compute grace or payout from different inputs. Frontend: the ready/not-owner branch of `_game_grid.html` and `GameFarm.js._plotHtml` render a `btn-danger` Steal button (`data-game-action="steal"`, `data-confirm` gated like prestige) carrying `steal_coins`, or a disabled "Raid again in " label when on cooldown; on success `GameFarm.js._submit` toasts the payout from `data.stole_coins`. +`store.steal(thief, owner, slot)` mirrors `water`: it requires the owner plot to be `ready` AND past the protection window `economy.effective_steal_grace(owner_defense_level)` (base `STEAL_GRACE_SECONDS` 60s, +30s per owner Branch Protection level) measured from `ready_at`, AND that the thief is **off cooldown for this victim** (`steal_cooldown_remaining(thief, owner, now) == 0`, i.e. no row in `game_steals` for the pair within `STEAL_COOLDOWN_SECONDS` = 3600 - you can raid a given neighbour only once per hour); it takes a **share** of the build rather than destroying it: `conditional_update_row` adds the share to the plot's `raided_fraction` (new `game_plots` REAL column, default 0, `COALESCE`d everywhere) and the crop stays in place, so the owner still harvests `1 - raided_fraction` of its value (`_harvest_crop` takes a `raided_fraction` argument). A build is raidable until the shares reach 1.0, after which `steal_reason` is `"stripped"`. The thief's payout is `realizable_harvest_coins(...) * share`, **clamped to the un-raided remainder** so repeated raids can never mint coins (a property sweep proved the un-clamped `max(1, round(...))` overshot by 1 coin on a fully stripped build). Raids are additionally capped at `STEAL_MAX_PER_VICTIM_PER_DAY` (3) per victim per day via `raids_against_today` over the new `idx_game_steals_owner_time` index, so an offline player cannot be stripped by an unlimited queue of raiders. Payout is **coins only** - no XP, no `total_harvests`, so the leaderboard stays earned by real farming - then inserts a `game_steals` row stamping the cooldown. The route `POST /game/farm/{username}/steal` (reusing `GameSlotForm`) then `track_action`s both sides, fires `create_notification(owner, "harvest_stolen", "Someone raided your Code Farm...", thief_uid, "/game")` (the message never names the thief; `related_uid` is internal), and `await notify_farm`s **both** the owner and the thief usernames so both farms refresh live. The new `harvest_stolen` notification type rides the existing in-app relay (live toast, no new wiring). No DB migration: grace + payout are computed from existing `ready_at`/perk/legacy columns, the `game_steals` table is created by the ensure-block (absent rows -> cooldown 0 -> first steal always allowed), and the new `GamePlotOut.can_steal`/`steal_coins`/`steal_cooldown_seconds`/`steal_reason` + `GameFarmOut.steal_cooldown_seconds` + `GameFarmViewOut.stole_coins` schema fields default safe. The per-pair cooldown is computed **once per farm** in `serialize_farm` (when the viewer is not the owner) and threaded into every `serialize_plot` as `steal_locked_until`, so `can_steal` is false and `steal_reason` is `"cooldown"`/`"protected"` accordingly. **Display must equal enforcement:** `serialize_farm` also threads the owner's Defense building into every `serialize_plot` (`steal_grace_bonus` = the tier's `grace_bonus`, `steal_floor` = the tier's `steal_fraction_floor` raised to `OBSERVABILITY_STEAL_FLOOR` when the owner owns Observability), so the serialized `can_steal`/`steal_coins` match exactly what `store/actions.py::steal()` will enforce and pay - a client acting on `can_steal=true` can never get a "still protected" 400, and `steal_coins` is the exact payout. Never let `serialize_plot` and `steal()` compute grace or payout from different inputs. Frontend: the ready/not-owner branch of `_game_grid.html` and `GameFarm.js._plotHtml` render a `btn-danger` Steal button (`data-game-action="steal"`, `data-confirm` gated like prestige) carrying `steal_coins`, or a disabled "Raid again in " label when on cooldown; on success `GameFarm.js._submit` toasts the payout from `data.stole_coins`. ## Extended mechanics (all backwards compatible) @@ -41,7 +41,9 @@ Five further systems layer onto the base loop, every one defaulting gracefully f 1. **Daily bonus** (`POST /game/daily`, `store.claim_daily`): once per UTC day, consecutive days grow `streak` (reward `economy.daily_reward`, capped at `DAILY_STREAK_CAP`). 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.) +4. **Fertilizer** (`POST /game/fertilize`, `store.fertilize`): cut a growing plot's `ready_at` by `FERTILIZE_FRACTION` for `economy.fertilize_click_cost(realizable_coins, reduce_seconds, full_grow_seconds)` = `ceil(realizable_coins * reduce/full_grow * FERTILIZE_TAX)` (TAX 1.05). + + **`economy.realizable_harvest_coins(...)` is the single source of truth for what a specific build pays, and BOTH the payout and the fertilize price must come from it.** It composes, in the exact order `_harvest_crop` pays out: `effective_reward_coins` (yield perk, prestige, legacy multiplier, market factor, underdog) then `GOLDEN_MULTIPLIER`, then `WEEKLY_CONTRACT_BOOST_MULTIPLIER`, then the Canary upside `1 + CANARY_DOUBLE_CHANCE`. Pricing against the *base* reward instead was a **live money pump**: golden builds (visible to the owner while growing via `serialize_plot.is_golden`) paid 5x while costing 1.05x, and even blind fertilizing turned profitable from ~prestige 5 on `E[golden] = 1.2 > 1.05`. Verified by a property sweep over every crop x prestige 0-200 x perk/legacy/market/boost combination: full-skip cost >= payout everywhere, and >= expected payout for the random Canary roll. **If you add any new harvest multiplier, it MUST be added to `realizable_harvest_coins` - adding it to `_harvest_crop` alone reopens the pump.** 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. @@ -89,7 +91,7 @@ A new, genuinely new table `game_market_ticks` (`crop_key`, `hour_bucket` = UTC ### 2. Coin sinks: Infrastructure, Defense, Cosmetics (`economy.py` + `store/infrastructure.py`, `store/defense.py`, `store/cosmetics.py`) - **Infrastructure** (`POST /game/infrastructure/buy`, `store.buy_infrastructure`): three one-time, boolean-owned, prestige-gated buildings (`economy.INFRASTRUCTURE`, new `game_farms` columns `infra_registry`/`infra_canary`/`infra_observability`). `registry` (+15% grow speed for Rust/Compiler/Kernel, `economy.REGISTRY_BOOST_FACTOR`, wired into `grow_seconds_for`'s new `registry_boost` bool param and into water's bonus-seconds calc). `canary` (`store/infrastructure.py::roll_canary`, a plain `random.random()` roll per harvest - deliberately NOT the deterministic `is_golden` hash, since canary is a fresh roll every harvest, not a per-planting property - `CANARY_DOUBLE_CHANCE` 12% to double, `CANARY_FAIL_CHANCE` 6% to only refund the planting cost). `observability` (raises the steal-fraction floor from 0.10 to `OBSERVABILITY_STEAL_FLOOR` 0.30 for this owner; the original critique's "see raid attempts in real time" was dropped - no scouting/detection mechanic exists anywhere in the game, and this achieves the same "raids feel less brutal" goal without inventing one). -- **Defense** (`POST /game/defense/upgrade`, `store.upgrade_defense`/`charge_upkeep`): a leveled building (`economy.DEFENSE_TIERS`, new columns `defense_level`/`defense_last_upkeep_at`) that lowers the steal-fraction floor and raises steal grace per tier (`effective_steal_fraction`/`effective_steal_grace` both gained a `floor`/`extra_seconds` parameter, defaulting to today's values, combined additively with the pre-existing Legacy `defense_level`). Upkeep is the deliberate whale sink: `economy.daily_upkeep(tier, coins) = max(tier.upkeep_daily, coins * UPKEEP_WEALTH_PCT)` (0.2%/day) so a large balance pays real money, not a trivial flat fee. `charge_upkeep` runs **lazily inside `serialize_farm`**, in the exact same spot and spirit as `_auto_harvest` (no new tick, no background service): unpaid upkeep (past `UPKEEP_GRACE_DAYS`) decays the tier by one level instead of going negative. +- **Defense** (`POST /game/defense/{upgrade,downgrade}`, `store.upgrade_defense`/`downgrade_defense`/`charge_upkeep`): a leveled building (`economy.DEFENSE_TIERS`, columns `defense_level`/`defense_last_upkeep_at`). **`DefenseTier.steal_reduction` is what actually works** - a multiplicative cut (0/5/12/20/30%) applied inside `effective_steal_fraction(defense_level, floor, building_reduction, cap)`. The older `steal_fraction_floor` is a *lower* bound and was **mathematically inert**: Legacy Branch Protection caps the subtractive term at 0.25, above every tier's floor, so the raider's share was byte-identical across all five tiers - players bought 2.3M coins of building plus 100k/day upkeep for nothing. Never reintroduce a "benefit" expressed as a floor. `cap` is the Observability Suite's `OBSERVABILITY_STEAL_CAP` (0.20); it replaced `OBSERVABILITY_STEAL_FLOOR` (0.30), which for a fully-defended owner *raised* the raider's share from 25% to 30% - a 150M coin purchase that actively harmed its buyer. Upkeep is the whale sink: `economy.daily_upkeep(tier, coins) = max(tier.upkeep_daily, coins * UPKEEP_WEALTH_PCT)` (0.2%/day). `charge_upkeep` runs **lazily inside `serialize_farm`**, same spot and spirit as `_auto_harvest` (no new tick): it charges **every elapsed day** (previously capped at `UPKEEP_GRACE_DAYS`, which made being absent cheaper than playing), and when the balance cannot cover it, takes `MIN(coins, due)` and decays one tier instead of zeroing the balance, then notifies via `create_notification(..., "game_upkeep", ...)`. The one-time `upkeep_amnesty` column (set to 1 in `ensure_farm` for new farms, NULL/0 for pre-existing ones) stamps `defense_last_upkeep_at` and charges nothing on the first read after deploy, so the switch to symmetric charging never retro-bills a live player. - **Cosmetics** (`POST /game/cosmetics/{buy,equip}`, `store/cosmetics.py`, new table `game_cosmetics` since this catalog is meant to grow - unlike the fixed Infrastructure/Defense tiers): pure-status titles and plot skins (`economy.COSMETICS`), zero gameplay effect. `game_farms.active_title` holds the equipped title key; `economy.cosmetic_title_name(key)` resolves it to a display name everywhere a leaderboard entry surfaces `title` (never render the raw key). Title display is scoped to the Code Farm leaderboard/farm view only - it does not touch `_avatar_link.html` or any sitewide identity surface. ### 3. Mastery track and new crop families (`economy.py` prestige/mastery/crops sections + `store/mastery.py`) @@ -127,3 +129,47 @@ Refactoring is no longer free at level 10 - it is the economy's progressive weal ### 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`). + +## The 2026 fairness and economics pass (`cf.md`) + +A full audit (`cf.md` at the repo root) found 28 issues; all were fixed. The load-bearing +invariants that came out of it, in addition to those already stated above: + +- **One quantity, one pure function.** Every number a player sees (price, payout, protection, + reward) is produced by exactly one function in `economy.py` and called with identical arguments + by the serializer and the mutator. Six of the 28 findings were the same defect - a value + computed in two places from different inputs (fertilize price vs payout, steal display vs + enforcement, the fertilize display omitting `registry_boost`). `serialize_plot` and the action + that charges/pays MUST share their inputs; thread a new parameter through both or neither. +- **Never express a player benefit as a lower bound** on something the player wants smaller (see + the Defense floor above). Model reductions multiplicatively and caps as caps. +- **Reward units are not goal units.** `weekly_contract` paid `goal // 40` Stars, which for the + coin-denominated `earn` contract meant **55 Stars for a single harvest** against 5 for a full + refactor and 419 for the whole Legacy tree. Contract rewards are now per-kind constants + (`QuestDef.contract_stars`/`contract_xp`) and the coin goal scales by the farm's own income + multiplier. Never divide a reward by a goal whose unit varies. +- **Flat rewards die.** Water/daily/quest payouts are scaled by `economy.social_reward_scale` + (the earner's OWN prestige x legacy multiplier, never the target's - scaling by the target's + would make watering a second raid mechanic). The flat constants remain the prestige-0 case. +- **Leaderboard coin term is capped** (`SCORE_COIN_CAP`): uncapped, `coins // 20` dominated + prestige 20:1 at endgame, so the default board ranked hoarding and refactoring *cost* you rank + while the `fair_play` board on the same selector penalized the same hoarding. +- **Market saturation is per-capita** (`supply_days(crop, recent, active_farms)`): absolute + thresholds let four active farms pin every crop at the 40% floor for the entire server. +- **`credit_farm`/`conditional_update_farm` are the only ways to move a counter.** `end_era` + awarded Stars with a read-modify-write (`stars = _lvl(farm,"stars") + award`), the exact + lost-update this file already warned about for `claim_quest`. It is now an atomic increment. +- **Auto-harvest must award what a manual harvest awards.** `_auto_harvest` returns a summary and + `routers/game/_shared.state_payload` fires `award_rewards`/`track_action` from it; without that, + buying the CI Bot silently forfeited all site XP and badge progress. +- **Farm XP is divided by `GAME_SITE_XP_DIVISOR` before becoming site XP.** One Kernel harvest is + 400 farm XP against 10 site XP for a published post; undivided, the idle game decided site level + and every level badge. +- **Number formatting is one rule with two implementations** that must agree character-for- + character: `templating.format_*` (server) and `static/js/Format.js` (client). `GameFarm.js` + re-renders the same hosts the server rendered, so any divergence is a visible flash. A unit test + asserts both against a shared value table. + +`raided_fraction` (`game_plots`), `upkeep_amnesty` (`game_farms`), and `joined_at` +(`game_era_results`) are the only new columns; all default to a value that makes an untouched row +behave exactly as before, and `idx_game_steals_owner_time` is the only new index.