Files
devplacepy/devplacepy/services/game/CLAUDE.md
T
2026-07-23 01:14:10 +02:00

36 KiB

Code Farm game (devplacepy/services/game/, devplacepy/routers/game/)

This file documents the Code Farm idle game. Claude Code auto-loads it when a file under devplacepy/services/game/ is read or edited.

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,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.

Data layer

Data layer is pure + timestamp-driven (no background tick). services/game/economy.py holds every constant and formula as frozen dataclasses/functions: the CROPS tuple (key/name/icon/cost/grow_seconds/reward_coins/reward_xp/min_level), CI_TIERS (speed multiplier + upgrade cost), level thresholds (xp_threshold/level_for_xp/level_progress), plot_cost (doubles per extra plot), and watering bonus. services/game/store.py is the only DB access (game_farms, game_plots). A plot's state is derived from ready_at vs now, never stored - growing crops finish purely by the clock, so there is no reconciler/service. serialize_farm(farm, viewer=, owner=) computes plot states, remaining seconds, can_water (viewer is not owner, build growing, viewer not already in the per-cycle watered_by JSON list, under MAX_WATERS_PER_PLOT), can_steal/steal_coins (viewer is not owner, build ready, and now >= ready_at + STEAL_GRACE_SECONDS so the owner gets a protection window), level progress, and the plantable crop list. serialize_plot takes the owner farm's yield_level/prestige (threaded from serialize_farm) only to compute the steal payout. Mutations (plant/harvest/buy_plot/upgrade_ci/water/steal) raise GameError on any invalid op (insufficient coins, locked crop, wrong state, still-protected harvest); the routers translate that to a 400 JSON error or a redirect.

Tables

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/)

index.py is the base router - GET /game (own farm page), GET /game/state (own farm JSON), GET /game/leaderboard, and the action POSTs plant/harvest/buy-plot/upgrade. farm.py adds GET /game/farm/{username} (view), POST /game/farm/{username}/water, and POST /game/farm/{username}/steal. Every action returns the full updated farm as JSON (so the client refreshes in one round trip) or redirects for no-JS, via the shared _respond_action choke (the farm.py water/steal handlers inline the same shape). Harvest awards site XP (award_rewards) and the harvest/water achievements (track_action). All action handlers are require_user; reads of other farms/the leaderboard are public.

Leaderboard ranking is a composite economy.farm_score(farm) (one integer per row, computed in memory over the already-loaded _farms().find() set, so it stays fast): it sums weighted contributions from every tracked factor - xp, prestige * SCORE_PRESTIGE (5000, dominant since a refactor is a full completed cycle), total_harvests * SCORE_HARVEST, coins // SCORE_COIN_DIVISOR, (ci_tier-1) * SCORE_CI, (plot_count-STARTING_PLOTS) * SCORE_PLOT, the summed perk levels * SCORE_PERK, and min(streak, SCORE_STREAK_CAP) * SCORE_STREAK - so a player who refactored (which resets xp/level/coins/ci/plots/perks) is no longer buried below a never-refactored higher-level player. The weights are module-level constants in economy.py for tuning; the leaderboard entry carries score and prestige (surfaced on GameLeaderboardEntryOut) and GameFarm.js renders the score next to Lv X.

Live + frontend

After any mutation the handler awaits _shared.notify_farm(username) which publishes a nudge to the public.game.farm.{username} pub/sub topic; subscribed clients re-fetch their viewer-specific state (keeps can_water correct without broadcasting per-viewer payloads). static/js/GameFarm.js (app.gameFarm, auto-detects [data-game-root]) renders the grid/HUD/leaderboard, ticks plot countdowns client-side every second, delegates the data-game-action forms through Http.send, subscribes to the farm topic, and keeps a 20s Poller fallback. The server renders a full no-JS fallback grid (templates/_game_grid.html, shared by game.html and game_farm.html with progressive-enhancement POST forms).

Fan-out

Devii plays via the game_* http actions in actions/catalog.py (state/leaderboard/view public, the rest requires_auth); the API reference has a Code Farm group in docs_api.py; pages are noindex,follow (interactive, user-specific) so they are intentionally not in the sitemap; badges live in utils.BADGE_CATALOG under the Code Farm group with harvest/water/harvest_stolen/got_stolen_from ACHIEVEMENTS (the steal pair awards Cat Burglar to the thief and Robbed to the victim, both threshold 1).

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_actions 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_farms 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. 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)

Five further systems layer onto the base loop, every one defaulting gracefully for pre-existing game_farms/game_plots rows: new game_farms columns (prestige, streak, last_daily_at, perk_yield/perk_growth/perk_discount/perk_xp) are added in the init_db ensure-block, and store._lvl(farm, key) reads every one as int(farm.get(key) or 0) so a legacy NULL row behaves as level 0 / no streak / no perks.

  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.)
  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.

Endgame: Stars, Legacy upgrades, auto-harvest, golden builds (all backwards compatible)

The infinite progression for maxed farms. Six new default-0 game_farms columns (stars, legacy_autoharvest, legacy_multiplier, legacy_speed, legacy_plots, legacy_defense), all read via _lvl.

Stars are a meta-currency earned only on Refactor (stars_for_refactor), spent via POST /game/legacy (store.upgrade_legacy, GameLegacyForm, Devii game_upgrade_legacy) on economy.LEGACY_UPGRADES (escalating legacy_cost in Stars) that survive prestige unlike perks: autoharvest (CI Bot), multiplier (+10% coins/lvl via legacy_multiplier, folded into effective_reward_coins), speed (+5% base build speed/lvl via farm_speed/grow_seconds_for/water_bonus_seconds), plots (+1 base plot after refactor via prestige_base_plots), defense (steal grace/fraction via effective_steal_grace/effective_steal_fraction).

Auto-harvest is lazy and tick-free: store._auto_harvest(farm, owner_uid, now) runs at the top of serialize_farm ONLY when the viewer is the owner AND legacy_autoharvest > 0; it clears each ready plot first then credits the pre-clear crop's coins/xp/total_harvests in one _update_farm, advances quests, and re-reads the farm - clear-then-credit is idempotent (a second read sees empty plots), and it never fires on a visitor's GET /game/farm/{username} view or the leaderboard, honouring the no-background-service invariant (both GET /game and GET /game/state go through state_payload with viewer=owner, so both auto-collect).

Golden builds are deterministic and storage-free: economy.is_golden(plot_uid, planted_at) (sha256, ~GOLDEN_CHANCE) marks a planting golden for its life; harvest/_auto_harvest multiply coins only by GOLDEN_MULTIPLIER (XP unscaled to keep level pacing), and serialize_plot.is_golden surfaces a sparkle badge (.game-plot-golden). New schema fields (GameLegacyOut, GameFarmOut.stars/legacy, GamePlotOut.is_golden) all default safe; no DB migration.

The economy rebalance (Market Saturation, Infrastructure, Defense, Cosmetics, Mastery, secondary leaderboards, Eras, Underdog, weekly Contracts - all backwards compatible)

A second large layer added on top of everything above, aimed at flattening runaway whale inequality (unbounded multiplicative prestige against finite content) and giving the game long-term excitement beyond "hit the Kernel + high-prestige wall." Every mechanic below defaults to a no-op for a farm that has never touched it - new game_farms columns are 0/"", new tables start empty, and the Era system in particular is fully dormant (byte-identical to pre-rebalance behavior) until an admin starts the first Era.

One shared choke point underlies all the new counters. store/common.py::credit_farm(farm, *, coins=0, xp=0, harvests=0, era_active=False, extra=None) is the single place that updates coins/xp/level/total_harvests and the new shadow counters (lifetime_coins_earned, lifetime_harvests, and - only when era_active - era_coins/era_harvests) together, so no future coin-crediting feature has to remember to touch five counters at five call sites. harvest, _auto_harvest, steal (thief side), claim_daily, and claim_quest all route through it instead of their old inline _update_farm math. credit_farm floors both coins and lifetime_coins_earned at zero (max(0, ...)) as a defense-in-depth guard - no current caller passes a negative coins delta, but this is the shared choke every future coin-crediting feature is meant to route through, so the floor costs nothing and forecloses a whole class of future bug.

Every purchase/upgrade is atomic against concurrent requests (store/common.py::conditional_update_farm)

uvicorn --workers N means two nearly-simultaneous requests for the same user (a double-click, or two requests hashed to different workers) can run truly in parallel across separate processes - a plain "read the farm, check a precondition in Python, then write" is a classic TOCTOU race under that model. Every mutation that spends a resource against a precondition - buy_plot, upgrade_ci (store/farm.py), upgrade_perk, upgrade_legacy, prestige (store/actions.py), buy_infrastructure, upgrade_defense, charge_upkeep (lazy, runs on every serialize_farm), buy_cosmetic, and upgrade_mastery - goes through conditional_update_farm(farm_uid, set_clause, where_clause, params): one raw SQL UPDATE game_farms SET ... WHERE uid = :farm_uid AND (<precondition>), returning the affected row count via db.executable.execute(...).rowcount (not dataset's wrapped db.query(), which does not expose rowcount). The precondition and the write happen as a single atomic statement, so two concurrent attempts against the same stale precondition can never both succeed - the loser's rowcount is 0, and the function re-reads the farm to produce the correct, specific GameError (already owned / maxed out / insufficient funds / state changed - refresh and retry).

Load-bearing gotcha: every precondition column must be COALESCEd. None of prestige, perk_*, legacy_*, stars, mastery_*, infra_*, or defense_level are set in ensure_farm's insert - a real, never-yet-touched farm has them as SQL NULL, not 0, and NULL = 0 evaluates to NULL (false) in a WHERE clause. A first version of this fix compared bare column = :current_level and was verified "safe" only because the verification script had incorrectly pre-seeded those columns to 0 - against a genuinely fresh farm it silently rejected every legitimate first purchase. Every precondition and every arithmetic SET on a column that is not set at farm creation must read COALESCE(column, 0); coins, plot_count, and ci_tier are the only farm columns set in ensure_farm and are the only ones safe to compare bare.

buy_plot additionally reserves the plot slot atomically (WHERE plot_count = :current_count AND coins >= :cost) before inserting the game_plots row - idx_game_plots_farm (farm_uid, slot_index) is not a unique index, so without the reservation two concurrent buys could insert two rows at the same slot_index (silent data corruption, not just a rejected purchase). prestige reserves the whole refactor transition atomically first (WHERE COALESCE(prestige, 0) = :old_prestige, in the same statement as every other reset field) and only mutates game_plots after that reservation wins, so a losing concurrent prestige call touches no plot data at all. Verified by forcing real concurrent OS processes (not threads - dataset gives each thread its own pooled connection, and enough of them exhausts the pool and produces database is locked noise that has nothing to do with the game logic) against genuinely fresh, un-seeded farm state; every purchase's final coins/stars/level exactly matches hand-computed expected totals under contention, never a partial or double charge.

1. Market Saturation (economy.py + store/market.py)

A new, genuinely new table game_market_ticks (crop_key, hour_bucket = UTC "YYYY-MM-DDTHH", harvests count, unique on (crop_key, hour_bucket)) tracks the last MARKET_WINDOW_HOURS (48h) of production per crop, globally - recorded only for owner harvests/auto-harvests (store/actions.py calls market.record_harvest_tick after a successful clear), deliberately not for steals (a raid moves already-produced value, it does not print new supply, so counting it would double-count). market.recent_harvests(crop_key, window_hours) sums the trailing buckets via a raw SELECT SUM (cached 30s in a plain TTLCache, cosmetic/economic staleness only, no cache_state wiring needed). economy.market_saturation_factor(recent_harvests) steps the payout down through MARKET_SATURATION_TIERS (100% -> 40% past 600 recent harvests); economy.market_buff_factor(crop_key, saturation_factor) gives the four starter crops (MARKET_BUFFED_CROPS) up to MARKET_BUFF_CAP (+15%) relief while the market is saturated. market.market_factor_for(crop_key) composes both into one multiplier, threaded as the new market_factor parameter on economy.effective_reward_coins/steal_reward_coins/crop_payload (default 1.0, so any caller that omits it is unaffected). GameCropOut.market_state ("normal"/"saturated"/"boosted") surfaces the live signal in the shop list before planting - both the server-rendered _game_grid.html plant <option> and its GameFarm.js JS mirror append a " (saturated)"/" (boosted)" text suffix to the crop label (an <option> cannot hold markup, so this is plain text, not a styled badge). market.prune_ticks(older_than_hours=96) keeps the table small; CLI devplace game market prune.

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.
  • 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)

Orthogonal to prestige, modeled directly on the existing Legacy shape (leveled, its own currency, survives prestige). Two-counter design, load-bearing: mastery_points (spendable, decreases on spend) vs mastery_points_earned_total (never decreases) - crop unlocks gate on the earned total so spending points can never re-lock content. economy.mastery_points_awarded(old_prestige, new_prestige) awards the FIRST Mastery point the moment prestige crosses MASTERY_UNLOCK_PRESTIGE (50) and one more every MASTERY_PRESTIGE_STEP (10) thereafter (_mastery_total_for(p) = 1 + (p-50)//10 for p>=50, difference of the before/after totals - correct even if prestige ever jumps by more than 1). store/actions.py::prestige() adds the award into the same reset dict, alongside stars, as a field that is not zeroed. POST /game/mastery (store.upgrade_mastery) spends mastery_points on economy.MASTERY_UPGRADES: autoreplant (Continuous Delivery - wired via a shared _try_autoreplant helper called from both harvest() and _auto_harvest() after crediting, replants the same crop immediately if still affordable/unlocked), analytics (Farm Analytics - GameFarmOut.mastery_analytics_unlocked gates the lifetime_coins_earned/lifetime_harvests stats panel, rendered by _game_analytics.html/data-analytics-host inside the Mastery section - both counters are accumulated by credit_farm, so they are exact from the moment the upgrade is bought, not backfilled for prior activity), contracts (Legacy Contracts - unlocks the weekly contract slot, see section 6; this deliberately consolidates the critique's "Legacy Contracts" and "weekly contracts" into ONE mechanism rather than two redundant long-goal systems).

Three new high-tier crops (distsys/mlpipe/secfort in economy.CROPS) extend the Crop dataclass with three new trailing, defaulted fields (min_mastery: int = 0, steal_immune: bool = False, era_key: str | None = None) - safe because every existing Crop(...) construction is positional over the original 8 non-defaulted fields. They set min_level=MAX_LEVEL (so, like every other crop, they re-lock after each refactor until the player re-levels to 20 - consistent with how Kernel etc. already behave post-prestige) and min_mastery=1 (gated on mastery_points_earned_total, checked both in store/actions.py::plant() - real enforcement - and in economy.crop_payload's locked flag - display). secfort (Security Fortress) is steal_immune=True: serialize_plot forces can_steal=False/steal_reason="immune" and store/actions.py::steal() raises immediately, before any grace/cooldown check, when the target crop is immune.

4. Secondary leaderboards (economy.py scoring + store/farm.py)

GET /game/leaderboard gained an optional board query param (default "score", so every existing caller/Devii/docs entry that omits it sees byte-identical output to before this shipped). store/farm.py::leaderboard_for(board, limit) dispatches through LEADERBOARD_BOARDS (score = the original farm_score board, prestige, harvests = this week's harvests_week counter, raids = average coins per successful raid with a minimum of MIN_RAIDS_FOR_EFFICIENCY_BOARD (3) qualifying raids - deliberately not "attempts," since failed steals are never persisted today and adding that write would be a spam vector for no real value, time_to_kernel = seconds between a farm's last prestiged_at and its next Kernel harvest, fair_play = economy.fair_play_score(harvests_week, coins) which rewards recent activity and penalizes hoarding) plus era (dispatches to store/era.py::leaderboard_era, always empty when no Era is running). GameFarm.js._loadLeaderboard renders a board-appropriate value per row instead of always showing the raw composite score - raid_avg (formatted "Nc/raid") for the raids board and a formatted duration for time_to_kernel, falling back to score for every other board. New game_farms columns backing these: harvests_week/harvests_week_start (reset lazily in serialize_farm via _reset_harvests_week whenever the current ISO week - common._iso_week - differs from the stored one, same lazy-no-tick idiom as everything else), prestiged_at (stamped every prestige()), last_kernel_harvest_prestige/time_to_kernel_seconds (updated in harvest/_auto_harvest the first time a Kernel is harvested since the last refactor).

5. Eras / seasons (economy.py era scoring + store/era.py, routers/admin/game.py)

Admin-triggered, not automatic - a deliberate deviation from "reset every 4-6 weeks": an unattended live-leaderboard reset in production is exactly the kind of surprising action this project treats with caution, so POST /admin/game/era/start and /era/end (/admin/game page, require_admin) are the only way an Era starts or ends. Two new tables: game_eras (at most one row active=1 at a time - zero active rows is the permanent no-op state, everything below is dormant until the first Era is started) and game_era_results (the permanent per-user historical record written once at era-end, before counters reset). Three new game_farms columns, era_coins/era_harvests/era_joined_at, are a shadow, resettable counter completely separate from the real, permanent coins/total_harvests - start_era bulk-resets only these three to zero/now for every farm; end_era resets era_coins/era_harvests back to zero for every participant it ranks (in the same _update_farm call that awards Stars, so a farm never carries a stale Era total into the next dormant period) while never touching real balances, prestige, stars, Legacy, or Mastery. While an Era is active, game.html's Era banner shows the owner's own live era_coins/era_harvests (via data-era-coins/data-era-harvests, kept current by GameFarm.js._renderHud on every action), not just the fact that a season is running. economy.era_score(era_coins, era_harvests, prestige) is a DISTINCT scoring function from economy.farm_score - it gives prestige only ERA_PRESTIGE_CARRYOVER_PCT (40%) weight so veterans keep an edge on the Era board specifically without it being insurmountable, while the permanent leaderboards (section 4) keep ranking by full lifetime stats untouched. end_era ranks every farm with any Era activity, awards economy.ERA_REWARD_STARS_BY_RANK Stars to the top 10 (added to the permanent stars, exactly like a refactor's stars_for_refactor) and, when the active Era name matches a Cosmetic.era_key, grants an Era-exclusive cosmetic. Era-exclusive crops reuse the Crop.era_key field from section 3 (unlocked_crops/crop_payload hide them unless the active Era's name matches - a no-op for every crop shipped so far, all of which have era_key=None).

6. Engagement: Underdog raids, weekly Contracts, client-side ready pings

Underdog (economy.UNDERDOG_COIN_RATIO=10, UNDERDOG_DURATION_HOURS=24, UNDERDOG_MULTIPLIER=1.25): in store/actions.py::steal(), if the owner's coins exceed 10x the thief's, the thief's underdog_boost_until is stamped; _harvest_crop applies the +25% multiplier while that timestamp is in the future. GameFarmOut.underdog_boost_seconds_remaining drives a HUD banner; steal()'s result carries underdog_triggered so routers/game/farm.py::steal_farm can fire the David vs Goliath badge.

Weekly Contracts extend the existing daily-quest engine (game_quests table) rather than building a second system: a new scope column ("daily"/"weekly", existing/new daily rows always "daily" - backfilled on migration via UPDATE game_quests SET scope='daily' WHERE scope IS NULL OR scope='' so in-flight quests from before this shipped are never duplicated) and reward_stars. store/quests.py::ensure_weekly_contract materializes one economy.weekly_contract(user_uid, iso_week) row per ISO week (deterministic sha256 pick, same idiom as daily_quests), gated on owning the mastery_contracts upgrade; advance_quests advances both the day's rows and the current week's row in one pass. claim_quest(user, kind, scope="daily") claims either; a weekly claim pays Stars plus a contract_boost_until timestamp that grants WEEKLY_CONTRACT_BOOST_MULTIPLIER (1.2x) coins for WEEKLY_CONTRACT_BOOST_HOURS (48h), applied in _harvest_crop the same way as the Underdog boost.

"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 COALESCEd, 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).