|
|
|
@@ -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 <countdown>" 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 <countdown>" 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.
|
|
|
|
|