Files
devplacepy/happyplanting.md
T
retoorandClaude Sonnet 5.5 02ab747cd0 Code Farm: per-plot auto-replant pause and crop-lock check; community sync and node work
Continuous Delivery could never be turned off once a crop was planted. Add a
one-shot per-plot "stop after harvest" flag (POST /game/plot/stop, Devii tool
game_plot_stop, game_plots.stop_after_harvest), cleared by the next plant,
autoreplant claim or Refactor. Autoreplant now also respects crop locks through
the shared _crop_lock predicate. Plan and measurements in happyplanting.md.

Also includes the pending community sync, node network and related work in the
tree; the full suite passed (4999) with all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 02:23:43 +02:00

11 KiB

happyplanting: fixing Continuous Delivery lock-in

Status: plan only. Nothing in this file is implemented. Written 2026-10-17 from a read of the code, not from a run of the game.

Problem (verified in code)

Continuous Delivery (autoreplant Mastery upgrade) replants the crop that was just harvested, immediately, whenever it is affordable.

  • store/actions.py::harvest and _auto_harvest call _try_autoreplant after crediting. The harvest pays before the replant is checked, so "affordable" is true in practice and the plot is never empty.
  • plant() refuses any plot that already holds a crop. There is no uproot, swap or cancel.
  • upgrade_mastery is one-way: no refund, no toggle. Mastery survives Refactor.
  • _try_autoreplant checks cost only. It ignores market saturation and crop locks.
  • Only exits today: Refactor (clears plots, costs a prestige-scaled fee) or running below the planting cost (rare).

Goals

  1. A per-plot "stop after this harvest" toggle, so the player gets one free crop choice on that plot.
  2. Do not replant into a bad market.

Part A: per-plot "stop after harvest" (ship this)

Design

New column game_plots.stop_after_harvest (int, default 0, always read with COALESCE). Added to the existing plot column ensure-block in database/schema.py (next to raided_fraction, ~line 1508) with example value 0.

Semantics: one-shot, per plot. While set, the next harvest of that plot does not replant. The plot ends empty and the player plants whatever they want.

Why this is the race-free shape

The flag is read by _try_autoreplant from its own fresh _plot_at(...) re-read, which already happens AFTER clear_plot wins. Rules that make that safe:

  • The toggle endpoint is one conditional UPDATE: SET stop_after_harvest = :v WHERE crop_key != '' AND crop_key IS NOT NULL, checked via rowcount (conditional_update_row). It cannot touch an already-cleared plot.
  • So the toggle either lands before clear_plot (the post-clear re-read sees it, no replant) or after (the WHERE fails, loud "plot is empty" error). No window where the harvest uses a stale flag.
  • clear_plot is NOT changed. A leftover 1 on an empty plot is harmless because every path that puts a crop on a plot writes stop_after_harvest = 0 in the same claim UPDATE: plant() and _try_autoreplant (both already use conditional_update_row with a SET string), and the prestige() plot reset.
  • Never reset the flag inside clear_plot: that would erase it before _try_autoreplant can read it.

Changes (fan-out, all four faces)

Layer File Change
Schema database/schema.py ensure game_plots.stop_after_harvest
Data services/game/store/actions.py _try_autoreplant: return early if COALESCE(plot.stop_after_harvest, 0); add stop_after_harvest = 0 to the claim SET in plant() and _try_autoreplant; add the column to the prestige() plot reset
Data services/game/store/ (new fn in actions.py) set_plot_stop(user, slot, stop: bool): owner only, requires mastery_autoreplant level > 0, requires a crop on the plot, GameError otherwise
Form models.py GamePlotStopForm(slot: int ge=0 le=64, stop: bool), modeled on GameSlotForm
Route routers/game/index.py POST /game/plot/stop, require_user, via _respond_action, audit game.plot.stop
Audit services/audit/events.md + category_for add game.plot.stop (same category as game.plant)
Schema out schemas/game.py GamePlotOut.stop_after_harvest: bool = False (owner only, default safe); serialize_plot sets it
View templates/_game_grid.html and static/js/GameFarm.js::_plotHtml a small toggle button on growing/ready plots, shown only when the viewer is the owner and owns Continuous Delivery; label "Stop after harvest" / "Resume auto-replant". Server render and JS mirror must match (the file already warns about flash on divergence). Reuse the existing data-game-action form delegation, no new JS class
Agent services/devii/actions/catalog/game.py game_plot_stop Action (requires_auth=True, params slot, stop). Not confirm-gated: reversible and costs nothing
API docs docs_api/groups/game.py endpoint() entry with sample_response
Prose docs templates/docs/code-farm.html Continuous Delivery row: document the toggle and that Mastery has no refund
Notes services/game/CLAUDE.md, README.md document the flag and the "plant/replant claim resets it" invariant

Gaps found in the second review (all must be in the change)

  • prestige() resets plots with an explicit dict in _plots().update({...}) (actions.py ~611). It does not touch unknown columns, so the dict must add "stop_after_harvest": 0 or a flag survives a Refactor.
  • A stale 1 on an EMPTY plot (left after a no-replant harvest) must never be shown: serialize_plot gates stop_after_harvest on crop_key present AND viewer is owner AND mastery_autoreplant owned.
  • Writers that put a crop on a plot are exactly two: plant() and _try_autoreplant (both conditional_update_row claims). Both SETs get stop_after_harvest = 0. _create_plot inserts need no change (column default via ensure-block, read with COALESCE).
  • Existing tests that enumerate things and will fail until updated: tests/unit/services/devii/actions/catalog.py (game action name list), locustfile.py (route coverage, the locust-maintainer agent owns it, never run a load test), events.md (audit catalogue), README.md tool list (line ~223) and the code-farm.html endpoint table (~line 433).
  • The toggle button is a new element in a dense plot card: needs CSS tokens only (no literals), must fit the smallest phone width, and needs a screenshot skill visual check in both server-rendered and JS-rendered forms.
  • notify_farm already runs in _respond_action, so the live refresh needs no extra wiring.
  • Interactions checked and unaffected: steal (never clears a plot), water, fertilize, CI Bot _auto_harvest (flag honoured the same way, via the post-clear re-read), Era/leaderboards, treasury/grant.

Not in scope for Part A

No farm-wide off switch and no refund of Mastery points. The per-plot toggle is the requested fix.

Part B: do not replant into a bad market (REVISED after second review: do not build the original)

Measurement

Computed with the real economy.realizable_harvest_coins at the saturation floor (market_factor 0.4), payout minus planting cost:

Prestige shell api kernel distsys mlpipe secfort
0 -1 0 +240 -480 -3600 -10800
50 +54 +1120 +18240 +56020 +101400 +229200
60 +65 +1344 +21840 +67320 +122400 +277200

Continuous Delivery needs prestige 50 (MASTERY_UNLOCK_PRESTIGE), where prestige_multiplier is 13.5. At that point even a fully saturated crop pays 11x or more its planting cost, for every crop in the catalog. Consequences:

  • The "profit floor" (skip when payout <= cost) can never fire for a real owner. It is dead code. Drop it.
  • A plain "skip when saturated" always makes the owner poorer: the idle plot earns 0, and a skipped replant also forfeits that cycle's XP, quest progress, weekly harvest count (grant eligibility, harvests and fair_play boards) and the market tick. Idle is never better than planting a saturated crop.
  • So the original Part B would be collateral damage, not a fix.

What to do instead

  1. Ship Part A only. It already solves the stated problem: one free crop choice per plot, whenever the player wants it, with zero economic downside.
  2. Optional, only if you still want saturation handling: an opt-in farm setting "when saturated, replant the best unsaturated unlocked crop instead". It never idles a plot. It needs a new pure function in economy.py that picks the crop (must define "best", for example highest realizable coins per second, among crops passing crop_lock_reason), used by both the serializer preview and _try_autoreplant (one-quantity-one-function rule). It changes which crop grows and its grow time, so it needs its own design pass and the Layer 1/2 property sweeps. Not recommended to bundle with Part A.

Risk list

  • Behavior change on deploy: Part A defaults to 0 for every existing plot, so nothing changes until a player clicks. There is no Part B behavior change (see revision above).
  • Missing column on first plant: the claim SET names stop_after_harvest, so a DB where init_db has not added the column would fail every plant. init_db runs before serving, and tests start from a fresh DB through it; verify with the layer-3 setup that uses a genuinely fresh, un-pre-seeded farm.
  • NULL trap: pre-existing plot rows will have stop_after_harvest NULL. Every read and every WHERE uses COALESCE(stop_after_harvest, 0).
  • Prestige reset: forget the column there and a stale 1 survives a Refactor. Covered by the claim-SET rule, but the reset dict must also write 0.
  • Visitors: the flag must never be exposed to non-owners (serialize_plot gates it on viewer == owner).
  • JSON vs template: a context key not on GamePlotOut is silently dropped from JSON. Add the field to the schema first.

Verification plan

Persisted tests (matching tiers; the suite is run by the orchestrating session, serial):

  • tests/api/game/mutations.py: toggle on, harvest, plot stays empty; toggle off, harvest, plot replanted; toggle on an empty plot is a 400; toggle without Continuous Delivery is a 400; a visitor cannot toggle; plant after a stop resets the flag; Refactor resets it.
  • tests/api/game/farm.py: visitor view of an owner's plots never shows stop_after_harvest.
  • tests/e2e/game/index.py: the toggle button renders and flips on click; server-rendered and JS-rendered markup agree.

Disposable scripts (scratch DB via DEVPLACE_DATABASE_URL and DEVPLACE_DATA_DIR, never production):

  • Layer 3 race: real OS processes firing toggle against harvest on the same plot, from a genuinely fresh farm (flag NULL, not pre-seeded to 0). Assert the replant happens exactly when the toggle landed after clear_plot, never when it landed before.
  • Layer 4: pyflakes/ruff check on every touched file.

Then the standard gates: clean from devplacepy.main import app import, em-dash grep on touched files, make test (all tiers) green, commit, and verify live on 127.0.0.1:10500.

Residual risk after the second review

  • Part A is a small, additive change with one new column, one new route and one guarded early return. I found no economic, concurrency or permission collateral in it. "No collateral damage" is a claim about the plan, not a measurement: it is proven only by the race script and the full suite above, neither of which has been run.
  • One accepted behavior: toggling "stop" on a plot that harvests in the same instant either stops the replant or returns "plot is empty". Both outcomes are correct and visible.

Decisions needed from you

  1. Part A: one-shot (recommended, flag cleared when the plot is next planted) or sticky until turned off?
  2. Part B: drop it (recommended), or commission the "replant best unsaturated crop" option as a separate piece of work?
  3. Fold in the crop-lock check fix in _try_autoreplant (docs promise it, code lacks it)?