This commit is contained in:
2026-07-23 01:14:10 +02:00
parent 64c3983c9f
commit 582e37d176
24 changed files with 549 additions and 36 deletions
+13 -3
View File
@@ -4,7 +4,7 @@ This file documents the Code Farm idle game. Claude Code auto-loads it when a fi
## Overview
`game/` package (routers, mounted at `/game`) is the **Code Farm** idle game (`index.py` + `farm.py`): `GET /game` (page), `GET /game/state`, `GET /game/leaderboard`, `POST /game/{plant,harvest,buy-plot,upgrade,fertilize,daily,perk,prestige,legacy,quests/claim}`, plus social `GET /game/farm/{username}` and `POST /game/farm/{username}/{water,steal}`.
`game/` package (routers, mounted at `/game`) is the **Code Farm** idle game (`index.py` + `farm.py`): `GET /game` (page), `GET /game/state`, `GET /game/leaderboard`, `POST /game/{plant,harvest,buy-plot,upgrade,fertilize,daily,grant,perk,prestige,legacy,quests/claim}`, plus social `GET /game/farm/{username}` and `POST /game/farm/{username}/{water,steal}`.
A cooperative-and-competitive idle game (Farmville-style) mounted at `/game`, member-only to play, public to view another farm. Cooperative loop = watering a neighbour's growing build; competitive loop = stealing a neighbour's ready build.
@@ -14,7 +14,7 @@ A cooperative-and-competitive idle game (Farmville-style) mounted at `/game`, me
## Tables
`game_farms` (one per user, `coins`/`xp`/`level`/`ci_tier`/`plot_count`/`total_harvests`, plus `prestige`/`streak`/`last_daily_at`, the four `perk_*` columns, and the endgame `stars` + five `legacy_*` columns), `game_plots` (`farm_uid`/`slot_index`/`crop_key`/`planted_at`/`ready_at`/`watered_by`), and `game_steals` (`thief_uid`/`owner_uid`/`slot_index`/`crop_key`/`coins`/`stolen_at`, the per-pair steal-cooldown ledger) are **not** soft-deletable - they are mutable game state consumed by state transitions, not user content. Columns + indexes are ensured in `database.init_db` (unique `idx_game_farms_user`, `idx_game_farms_rank`, `idx_game_plots_farm`, `idx_game_steals_pair` on `(thief_uid, owner_uid, stolen_at)`); the `_uid_index` loop adds the uid index. `ensure_farm(user_uid)` lazily creates a farm + starting plots on first access (idempotent), so there is no signup hook.
`game_farms` (one per user, `coins`/`xp`/`level`/`ci_tier`/`plot_count`/`total_harvests`, plus `prestige`/`streak`/`last_daily_at`/`last_grant_week`, the four `perk_*` columns, and the endgame `stars` + six `legacy_*` columns including `legacy_carryover`), `game_plots` (`farm_uid`/`slot_index`/`crop_key`/`planted_at`/`ready_at`/`watered_by`), `game_steals` (`thief_uid`/`owner_uid`/`slot_index`/`crop_key`/`coins`/`stolen_at`, the per-pair steal-cooldown ledger), and `game_treasury` (a single row `uid="treasury-main"` holding `balance`/`collected_total`/`granted_total`, the refactor-fee redistribution ledger) are **not** soft-deletable - they are mutable game state consumed by state transitions, not user content. Columns + indexes are ensured in `database.init_db` (unique `idx_game_farms_user`, `idx_game_farms_rank`, `idx_game_plots_farm`, `idx_game_steals_pair` on `(thief_uid, owner_uid, stolen_at)`); the `_uid_index` loop adds the uid index. `ensure_farm(user_uid)` lazily creates a farm + starting plots on first access (idempotent), so there is no signup hook.
## Routes (`routers/game/`)
@@ -42,7 +42,7 @@ Five further systems layer onto the base loop, every one defaulting gracefully f
2. **Daily quests** (`game_quests` table, `POST /game/quests/claim`): `economy.daily_quests(user_uid, day)` deterministically (sha256 of `user:day`) picks 3 of {plant, harvest, water, earn} with goals/rewards; `store.ensure_quests` lazily materializes the day's rows, `store.advance_quests(user_uid, kind, amount)` is called inside `plant`/`harvest`/`water` (wrapped in try/except so a quest write never breaks the action), `claim_quest` pays out when `progress >= goal`.
3. **Perks** (`POST /game/perk`, `store.upgrade_perk`): four permanent upgrades (`economy.PERKS`) with escalating `perk_cost`; applied in the economy formulas - `effective_plant_cost` (discount), `grow_seconds_for(crop, ci_tier, growth_level)` via `farm_speed` (growth), `effective_reward_coins` (yield + prestige), `effective_reward_xp` (xp).
4. **Fertilizer** (`POST /game/fertilize`, `store.fertilize`): cut a growing plot's `ready_at` by `FERTILIZE_FRACTION` for `economy.fertilize_click_cost(eff_reward, reduce_seconds, full_grow_seconds)` = `ceil(eff_reward * reduce/full_grow * FERTILIZE_TAX)` (TAX 1.05). **The cost is priced against the build's realized harvest value (`effective_reward_coins`, which already carries yield/prestige/legacy multipliers), not raw grow-seconds, so the prestige dependence cancels and fully fertilizing a crop always costs >= its harvest - fertilize is a pure time-skip and can NEVER be a profit at any prestige.** (This replaced the old grow-seconds-based `fertilize_cost`, which was an unbounded money pump at high prestige.)
5. **Prestige/Refactor** (`POST /game/prestige`, `store.prestige`): at `PRESTIGE_MIN_LEVEL` resets coins/xp/level/ci/perks, sets `plot_count = economy.prestige_base_plots(legacy_plots_level)` (keeps/recreates plot rows up to that base, deletes the rest), increments `prestige` for a permanent `prestige_multiplier` (+25% coins each), and awards `economy.stars_for_refactor(level, prestige)` Stars (the `legacy_*`/`stars` columns are **omitted from the reset dict** so they survive every refactor, the established pattern).
5. **Prestige/Refactor** (`POST /game/prestige`, `store.prestige`): at `PRESTIGE_MIN_LEVEL` resets coins/xp/level/ci/perks, sets `plot_count = economy.prestige_base_plots(legacy_plots_level)` (keeps/recreates plot rows up to that base, deletes the rest), increments `prestige` for a permanent `prestige_multiplier` (+25% coins each), and awards `economy.stars_for_refactor(level, prestige)` Stars (the `legacy_*`/`stars` columns are **omitted from the reset dict** so they survive every refactor, the established pattern). **Refactoring costs a dynamic coin fee** - see "Refactor fee, carry-over, treasury, and community grant" below.
`serialize_farm` exposes all of this (`perks`, `quests`, `streak`, `daily_available`/`daily_reward`, `prestige*`, `stars`, `legacy`, `steal_cooldown_seconds`) only to the owner; the existing `crop_payload`/`serialize_plot` now carry perk- and legacy-adjusted costs/rewards and per-plot value-based `fertilize_cost`. Frontend hosts (`[data-shop-host]`/`[data-perk-host]`/`[data-legacy-host]`/`[data-daily-host]`/`[data-quest-host]`) are server-rendered from partials (`_game_shop.html`/`_game_perks.html`/`_game_legacy.html`/`_game_daily.html`/`_game_quests.html`) and fully re-rendered by `GameFarm.js` builders; the generic `data-game-action` form delegation handles the new actions, with `data-confirm` gating the prestige reset.
@@ -102,6 +102,16 @@ Three new high-tier crops (`distsys`/`mlpipe`/`secfort` in `economy.CROPS`) exte
**"Crops ready" notifications are client-side only** (`GameFarm.js`'s existing 1-second ticker fires a toast/desktop `Notification` when a countdown hits zero while the tab is open) - this respects the hard "no background tick" architectural invariant, since nothing server-side needs to detect the transition. **"Someone is scouting you" was not built** - like Observability's real-time alert, it would require inventing a scouting/detection mechanic that does not exist. **Alliance/Farm Coop was deliberately deferred** to its own follow-up plan - it is a new multiplayer social feature (group membership, shared resource pooling, its own invite/leave/kick UX), qualitatively different from every economy-tuning change here, and was judged to deserve its own design pass rather than being folded into this rebalance.
### Refactor fee, carry-over, treasury, and community grant (`store/treasury.py`)
Refactoring is no longer free at level 10 - it is the economy's progressive wealth sink, and the sink funds a redistribution loop. All backwards compatible: new `game_farms` columns (`legacy_carryover`, `last_grant_week`) default NULL and are `COALESCE`d, and the `game_treasury` table starts empty (lazily seeded by `ensure_treasury`).
- **Dynamic fee** (`economy.refactor_cost(prestige, coins)` = `REFACTOR_BASE_COST` (20k) `* (1 + prestige) * prestige_multiplier(prestige) + REFACTOR_WEALTH_PCT` (15%) `* coins`): the polynomial prestige term makes time-to-afford grow linearly with prestige (a soft cap on prestige cycling - income grows only linearly, so an exponential fee would wall off Mastery), and the wealth term is a progressive tax a hoarder cannot dodge. At prestige 0 the fee means a level-10 farm must keep farming (~23.5k coins minimum) before its first refactor - instant level-10 cycling is dead.
- **Carry-over** (`economy.refactor_carryover(coins, fee, carryover_level)`): after the fee, `REFACTOR_CARRYOVER_BASE` (10%) of the remainder survives the reset, raised +5%/level by the new **Golden Parachute** Legacy upgrade (`legacy_carryover`, max 5 -> 35%). Always strictly below 100% so a refactor can never be net-profitable (same invariant class as the fertilize fix). `prestige()` pins the balance in the atomic WHERE (`AND coins = :coins_now`) so a concurrent harvest yields a "balance just changed - refresh" `GameError` instead of a wrong charge, then credits the fee to the treasury.
- **Treasury** (`game_treasury`, single row `uid="treasury-main"`, `balance`/`collected_total`/`granted_total`): every refactor fee is credited atomically (`credit_treasury`, raw conditional SQL like `conditional_update_farm`). The ledger invariant `balance == collected_total - granted_total` is fuzz-verified.
- **Community grant** (`POST /game/grant`, `store.claim_grant`): once per ISO week (`last_grant_week`), an **active low-wealth, low-prestige** farm (coins < `GRANT_WEALTH_CEILING` 10k, `harvests_week >= GRANT_MIN_WEEK_HARVESTS` 5 - the activity gate blocks idle alts, prestige <= `GRANT_MAX_PRESTIGE` 5 - so post-refactor whales with a briefly low balance never draw welfare) claims `economy.grant_amount(balance)` (capped `GRANT_CAP` 2,500). Order of operations: atomic treasury debit first (`WHERE balance >= :amount`), then the atomic farm credit+week-stamp; a losing farm update refunds the treasury. Race-verified with real OS processes: concurrent claims by one farm pay exactly once, and two farms racing an underfunded treasury never overdraw it.
- **Fan-out:** `serialize_farm` exposes `refactor_cost`/`refactor_affordable`/`refactor_carryover_pct`/`refactor_carryover_preview` plus the owner-only `grant_*`/`treasury_balance`; the shop row and `GameFarm.js._shopHtml` show the fee and hide the button until affordable; the sidebar **Community grant** section is `_game_grant.html`/`data-grant-host`/`_grantHtml`. Devii: `game_prestige` is now in `CONFIRM_REQUIRED` (large irreversible spend) with a declared `confirm` param; `game_claim_grant` is a plain auth tool. Docs: `game-prestige` updated + `game-grant` added.
### Admin surface
`/admin/game` (`routers/admin/game.py`, `admin_game.html`, sidebar entry between Bot Monitor and AI usage) shows the current Era status and the start/end forms; `AdminGameOut` is the response schema. `devplace game market prune` and `devplace game era {status,start,end}` mirror the same actions from the CLI (`cli/game.py`).
+45
View File
@@ -269,6 +269,12 @@ PERK_BY_KEY = {perk.key: perk for perk in PERKS}
PRESTIGE_MIN_LEVEL = 10
PRESTIGE_BONUS = 0.25
REFACTOR_BASE_COST = 20_000
REFACTOR_WEALTH_PCT = 0.15
REFACTOR_CARRYOVER_BASE = 0.10
REFACTOR_CARRYOVER_MAX = 0.95
LEGACY_CARRYOVER_STEP = 0.05
STAR_BASE = 1
LEGACY_SPEED_STEP = 0.05
LEGACY_MULT_STEP = 0.10
@@ -321,6 +327,15 @@ LEGACY_UPGRADES: tuple[LegacyUpgrade, ...] = (
2,
1.8,
),
LegacyUpgrade(
"carryover",
"Golden Parachute",
"🪂",
"+5% of your post-fee coins carried through each refactor per level",
5,
2,
1.8,
),
)
LEGACY_BY_KEY = {up.key: up for up in LEGACY_UPGRADES}
@@ -363,6 +378,9 @@ def legacy_value_text(up: LegacyUpgrade, level: int) -> str:
return f"+{round(LEGACY_SPEED_STEP * level * 100)}% build speed"
if up.key == "plots":
return f"+{level} starting plots"
if up.key == "carryover":
pct = round(refactor_carryover_fraction(level) * 100)
return f"{pct}% refactor carry-over"
grace = LEGACY_DEFENSE_GRACE * level
loss = round(LEGACY_DEFENSE_FRACTION * level * 100)
return f"+{grace}s grace, -{loss}% steal loss"
@@ -405,6 +423,33 @@ def prestige_multiplier(prestige: int) -> float:
return 1 + PRESTIGE_BONUS * max(0, prestige)
def refactor_cost(prestige: int, coins: int) -> int:
scaled = REFACTOR_BASE_COST * (1 + max(0, prestige)) * prestige_multiplier(prestige)
return round(scaled + REFACTOR_WEALTH_PCT * max(0, coins))
def refactor_carryover_fraction(carryover_level: int = 0) -> float:
return min(
REFACTOR_CARRYOVER_MAX,
REFACTOR_CARRYOVER_BASE + LEGACY_CARRYOVER_STEP * max(0, carryover_level),
)
def refactor_carryover(coins: int, fee: int, carryover_level: int = 0) -> int:
remainder = max(0, coins - fee)
return int(remainder * refactor_carryover_fraction(carryover_level))
GRANT_WEALTH_CEILING = 10_000
GRANT_MIN_WEEK_HARVESTS = 5
GRANT_MAX_PRESTIGE = 5
GRANT_CAP = 2_500
def grant_amount(treasury_balance: int) -> int:
return max(0, min(GRANT_CAP, treasury_balance))
UNDERDOG_COIN_RATIO = 10
UNDERDOG_DURATION_HOURS = 24
UNDERDOG_MULTIPLIER = 1.25
@@ -59,6 +59,7 @@ from .infrastructure import buy_infrastructure, owns_infrastructure
from .market import market_factor_for, prune_ticks, recent_harvests
from .mastery import upgrade_mastery
from .quests import advance_quests, ensure_quests
from .treasury import claim_grant, ensure_treasury, grant_status, treasury_balance
from .serialize import (
_daily_available,
_plot_state,
+23 -4
View File
@@ -488,6 +488,8 @@ def upgrade_legacy(user: dict, key: str) -> dict:
def prestige(user: dict) -> dict:
from .treasury import credit_treasury
farm = ensure_farm(user["uid"])
level = economy.level_for_xp(int(farm.get("xp", 0)))
if level < economy.PRESTIGE_MIN_LEVEL:
@@ -496,6 +498,13 @@ def prestige(user: dict) -> dict:
)
old_prestige = _lvl(farm, "prestige")
new_prestige = old_prestige + 1
coins_now = int(farm.get("coins", 0))
fee = economy.refactor_cost(old_prestige, coins_now)
if coins_now < fee:
raise GameError(
f"Refactoring costs {fee} coins right now - keep farming to afford it."
)
carried = economy.refactor_carryover(coins_now, fee, _lvl(farm, "legacy_carryover"))
stars_award = economy.stars_for_refactor(level, old_prestige)
mastery_award = economy.mastery_points_awarded(old_prestige, new_prestige)
base_plots = economy.prestige_base_plots(_lvl(farm, "legacy_plots"))
@@ -503,7 +512,7 @@ def prestige(user: dict) -> dict:
now = _iso(now_dt)
set_parts = [
"coins = :starting_coins",
"coins = :post_coins",
"xp = 0",
"level = 1",
"ci_tier = 1",
@@ -515,13 +524,14 @@ def prestige(user: dict) -> dict:
"prestiged_at = :now",
]
params = {
"starting_coins": economy.STARTING_COINS,
"post_coins": economy.STARTING_COINS + carried,
"base_plots": base_plots,
"new_prestige": new_prestige,
"stars_award": stars_award,
"mastery_award": mastery_award,
"now": now,
"old_prestige": old_prestige,
"coins_now": coins_now,
}
for perk in economy.PERKS:
column = PERK_COLUMN[perk.key]
@@ -530,16 +540,19 @@ def prestige(user: dict) -> dict:
rows = conditional_update_farm(
farm["uid"],
set_clause=", ".join(set_parts),
where_clause="COALESCE(prestige, 0) = :old_prestige",
where_clause="COALESCE(prestige, 0) = :old_prestige AND coins = :coins_now",
params=params,
)
if rows == 0:
farm = get_farm(user["uid"])
if _lvl(farm, "prestige") != old_prestige:
raise GameError("Your farm already refactored - refresh and try again.")
if int(farm.get("coins", 0)) != coins_now:
raise GameError("Your coin balance just changed - refresh and try again.")
raise GameError(
f"Reach level {economy.PRESTIGE_MIN_LEVEL} to refactor (prestige)."
)
credit_treasury(fee)
kept_slots = set()
for plot in get_plots(farm["uid"]):
@@ -561,7 +574,13 @@ def prestige(user: dict) -> dict:
for slot_index in range(base_plots):
if slot_index not in kept_slots:
_create_plot(farm["uid"], user["uid"], slot_index, now)
return {"prestige": new_prestige, "stars_awarded": stars_award, "mastery_awarded": mastery_award}
return {
"prestige": new_prestige,
"stars_awarded": stars_award,
"mastery_awarded": mastery_award,
"fee": fee,
"carried": carried,
}
def fertilize(user: dict, slot: int) -> dict:
@@ -137,6 +137,7 @@ def serialize_farm(
) -> dict:
from .actions import _auto_harvest
from .defense import charge_upkeep
from .treasury import grant_status, treasury_balance
now = now or _now()
viewer_uid = viewer["uid"] if viewer else ""
@@ -193,6 +194,14 @@ def serialize_farm(
)
building_defense_level = _lvl(farm, "defense_level")
defense_tier_info = economy.defense_tier(building_defense_level)
coins = int(farm.get("coins", 0))
refactor_fee = economy.refactor_cost(prestige, coins)
carryover_level = _lvl(farm, "legacy_carryover")
grant = (
grant_status(farm, now)
if is_owner
else {"available": False, "amount": 0, "reason": ""}
)
return {
"owner_username": owner.get("username", ""),
"owner_uid": owner_uid,
@@ -240,6 +249,20 @@ def serialize_farm(
"prestige_multiplier": economy.prestige_multiplier(prestige),
"prestige_min_level": economy.PRESTIGE_MIN_LEVEL,
"prestige_available": is_owner and level >= economy.PRESTIGE_MIN_LEVEL,
"refactor_cost": refactor_fee,
"refactor_affordable": is_owner
and level >= economy.PRESTIGE_MIN_LEVEL
and coins >= refactor_fee,
"refactor_carryover_pct": round(
economy.refactor_carryover_fraction(carryover_level) * 100
),
"refactor_carryover_preview": economy.refactor_carryover(
coins, refactor_fee, carryover_level
),
"grant_available": grant["available"],
"grant_amount": grant["amount"],
"grant_reason": grant["reason"],
"treasury_balance": treasury_balance() if is_owner else 0,
"streak": streak,
"daily_available": daily_available,
"daily_reward": economy.daily_reward(streak + 1 if daily_available else streak),