Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

garden.gno

257.82 Kb · 4925 lines
   1// Package garden is Gnotchi's pet realm, v7: the free, allowlisted, drand-rolled, hold-many
   2// realm MINT.md designs end to end, now carrying Batch One's personality, the kinship web and a
   3// batch of sixty. It inherits garden/v4's machinery -- the SaveDay shape, meters, garden plots,
   4// levels, and the List/Buy/Cancel trading surface -- but NOT v4's invariants (MINT.md section 6).
   5// garden/v4 is live, immutable and untouched by this file; its two gnomes stay exactly where
   6// they are forever (MINT.md section 6, BATCH-ONE.md section 0), and so does garden/v5's one.
   7//
   8// ============================================================================================
   9// WHAT v7 IS: v6 PLUS EXACTLY TEN CHANGES. READ THIS, THEN "WHAT v6 IS" BELOW IT.
  10// ============================================================================================
  11//
  12// v7's specification is realms/garden/v7/DESIGN.md (1,974 lines, written 2026-10-17 and
  13// reviewed before any of this was typed). This block is the summary; that document is the
  14// argument, and where this file disagreed with it the disagreement is marked in the source at
  15// the place it happens.
  16//
  17// THE RULE THIS FILE IS WRITTEN UNDER, which is v6's own rule quoted back at its successor:
  18// "Every line of v5 that is not one of the two fixes below is carried forward byte-for-byte...
  19// v5 survived a security audit; every unrelated change made here would be a change that has not
  20// been audited, so none was made." v7 is the version that was reserved for changing things, so
  21// the door is open -- but THE AUDIT ARGUMENT DOES NOT EVAPORATE BECAUSE THE DOOR IS OPEN.
  22// Everything v6 carries forward is carried forward here too, byte-for-byte, unless it appears in
  23// the list below. The list is complete. If a change is not in it, it is not authorised, and the
  24// three defects mutation testing found in v5, along with every comment recording why something
  25// is shaped as it is, are all still here on purpose.
  26//
  27// CHANGE 1 -- batchCap 24 -> 60. DECISIONS.md 2026-10-17, "Batch One becomes 60", superseding
  28// "Batch One stays 24" from earlier the same day. ONE CONSTANT.
  29//
  30// CHANGE 2 -- FIVE ROLLED TRAIT AXES plus born-under, stored on the gnome. TRAITS.md sections 1
  31// and 7.
  32//
  33// CHANGE 3 -- THE DOMINANCE ORDER, stored as five explicit slots rather than a 7-bit Lehmer
  34// index. The reason is in the gnome struct's own comment: a factorial-base index needs a codec
  35// on both sides of the wire, which is two things that must agree forever over a convention
  36// nobody has written down, in the one function MINT.md section 9 names as needing its own audit
  37// pass above all others.
  38//
  39// CHANGE 4 -- THE FOUR STATS on a shared budget, drawn from a 546-row legal set ENUMERATED AT
  40// init() FROM THE RULE rather than transcribed, because a transcription error in a 546-row table
  41// is undetectable by eye and the rule it transcribes is four comparisons. TRAITS.md section 4.2.
  42//
  43// CHANGE 5 -- A SECRET ID, with the first-seventeen-distinct constraint. TRAITS.md section
  44// 3.2.2: without it, 36.9% of batches could not spell the acrostic of section 3.2.1.
  45//
  46// CHANGE 6 -- A ROLLED CARE THRESHOLD, stored, WITH NO GETTER. The one declared exception to
  47// GAMES.md:38, and the only write-only field in the realm.
  48//
  49// CHANGE 7 -- THE 84 KINSHIP EDGES on chain, STRUCTURE ONLY, keyed by batch index. LINEAGE.md
  50// section 6's OVERRULED box: "store the structure, keep the wording off chain where it can still
  51// be improved."
  52//
  53// CHANGE 8 -- seedFor MOVES FROM FNV-1a TO SHA-256. NOT A FEATURE -- a defect DESIGN.md found
  54// while working out change 3. FNV-1a's low bits are provably a parity of the input bytes' low
  55// bits; v5 and v6 take % 100, % 60 and % 4 and mostly get away with it, and v7 takes % 2, % 3,
  56// % 5 and % 16 for the one field TRAITS.md's whole duplicate-vector argument rests on. THIS IS
  57// THE ONE TO READ FIRST IN REVIEW: it rewrites a line an audited realm carries. Proof at
  58// seedFor.
  59//
  60// CHANGE 9 -- THE NAMED-EDITION ODDS ARE DECOUPLED FROM THE BATCH SIZE AND RAISED TEN-FOLD.
  61// 2026-10-18, the owner: "make the last few have them. The rates have to come out during a mint."
  62// ONE CONSTANT AND ONE LINE: namedPick's modulus was uint64(batchCap), so each of Hob, Thistle and
  63// Wick was a 1-in-60 shot per claimant and there was a 74.37% chance at least one of the three
  64// never existed. It is now uint64(namedPickRange), its own constant at 10 -- a 1-in-10 shot each,
  65// leaving 0.54% of full batches missing one. NOTHING ELSE ABOUT THE NAMED-EDITION PATH MOVES: the
  66// `namedPick < numNamedEditions` branch, the characterOwner.Has check and the
  67// transaction-ordering-race comment inside it are v6's byte-for-byte. THIS IS NOT A GUARANTEE and
  68// is not a replacement for the removed one; it is a probability, it fires nowhere new, and it reads
  69// nothing new. The arithmetic for all three rates, why the modulus is NOT batchCap (that coupling
  70// was accidental -- it made the rarest-asset odds a side effect of a batch size changed for
  71// unrelated reasons), why raising a rate is safe where the removed guarantee was not, and the
  72// honest cost (collisions become common; first settle wins) are at namedPickRange's declaration.
  73//
  74// CHANGE 10 -- THE NAMED ONE-OF-ONES GO FROM THREE TO EIGHT. 2026-10-18, the owner: "If we are
  75// doing 60 there should be more specials than 3." He is right, and the reason is arithmetic that
  76// change 1 produced silently: three of 24 was 12.5% of a batch, and moving the batch to 60 without
  77// touching the roster quietly cut it to 5%. EIGHT OF 60 IS 13.3%, which restores the proportion the
  78// collection was designed at. ONE CONSTANT (numNamedEditions 3 -> 8) AND THREE TABLES IN STEP
  79// (namedEditionOrder, namedEditionDisplayNames, reservedNameKeys) -- plus List's lock message,
  80// which says the count out loud and is therefore a FOURTH place the roster's size appears; see
  81// numNamedEditions for the full list of what must move together.
  82//
  83// THE FIVE NEW NAMES ARE BATCH-ONE.md section 7's, EXTENDED IN THAT SECTION'S OWN REGISTER AND
  84// JUSTIFIED THERE, NOT HERE: Tally, Glim, Vigil, Spruce, Yonder. They are EXISTING, ALREADY
  85// VALIDATED ART, renamed exactly the way Hob (the gilded gnome) and Thistle (the Viking) were --
  86// trowel-hand, emberwing, nightwatch, gentleman and pirate from pack/04-pixel-art/characters.json,
  87// every one of them "works"/"confirmed at true 2x" in ARCHETYPES.md section 3 and section 6. NO NEW
  88// ART WAS DRAWN OR MODIFIED FOR THIS CHANGE and this realm does not know what any of them look
  89// like: `character` is a string, the look lives in the art pack, and that separation is unchanged.
  90//
  91// namedPickRange STAYS AT 10, DELIBERATELY, AT THE OWNER'S APPROVED ODDS -- a specific edition is
  92// still a 1-in-10 shot per claimant and nothing about the pick, the branch or the race moves. What
  93// DOES move is what the odds compose to, and the new figures replace the old ones everywhere they
  94// are written down: (9/10)^60 = 0.18% per edition is unchanged, but P(at least one of EIGHT
  95// missing) = 1 - (1-0.0017971)^8 = 1.43%, up from 0.54% at three. A half-filled batch of 30 is
  96// 29.3%. The cost side moves much further: 48 of 60 claimants now hold a named-pointing pick
  97// (60 * 8/10) for eight slots, so about 40 of them lose the race and fall through to an ordinary
  98// draw. Full arithmetic, all four rows, at namedPickRange. NOTE THE HEADROOM: 10 >= 8 still holds,
  99// but only just -- at eight editions the modulus is two residues from the point where an edition
 100// becomes unreachable, so numNamedEditions may not grow again without raising namedPickRange, and
 101// TestNamedEditionOddsAreOneInTen is the test that fails if it does.
 102//
 103// NOT A GUARANTEE, AGAIN AND FOR THE SAME REASONS: this adds five entries to three tables and no
 104// mechanism. No forcing, no namedEditionToForce, nothing keyed on a shrinking observable (AUDIT-01
 105// RED-1). "TALLY WAS NEVER BORN" is now a 0.18% permanent fact per edition exactly as "HOB WAS
 106// NEVER BORN" is, and the chance that SOME edition is missing went UP, not down, because there are
 107// more editions to miss. That is the accepted price of the proportion the owner asked for.
 108//
 109// NOT A CHANGE -- THE NAMED-EDITION GUARANTEE, WRITTEN 2026-10-17 AND REMOVED 2026-10-18. It was
 110// briefly a change of its own: "when the number of unsettled slots remaining equals the number of
 111// named editions still unclaimed, the remaining settles take them." AUDIT-01 RED-1 found that
 112// rule positionable -- its trigger was computable from two public readers and a wallet that never
 113// claimed until 57 were settled took an edition with certainty -- and the owner chose REMOVAL over
 114// any replacement, so the named-edition path here is v6's byte-for-byte apart from change 9's one
 115// modulus, and NO END-OF-BATCH FORCING AND NO namedEditionToForce EXISTS OR MAY BE RE-ADDED. The
 116// residual chance that at least one edition is never minted is KNOWN AND ACCEPTED: 74.4% when
 117// it was accepted, 0.54% after change 9, 1.43% after change 10 raised the roster from three to
 118// eight (more editions, more chances for one of them to be missed), never zero. The full record of
 119// the decision is in settleCore, next to the pick it would have overridden; do not re-add a
 120// guarantee without reading it, and note that changes 9 and 10 are deliberately NOT one -- between
 121// them they move two numbers and five table entries, not a mechanism.
 122//
 123// WHAT v7 MUST NEVER DO, and these are named so that leaving them out is a decision on the
 124// record rather than an omission. From TRAITS.md section 7: no rarityScore(), no TraitCensus(),
 125// no traitCount(axis, id), no sort-by-stat, no trait-gated entrypoint anywhere in any realm,
 126// nothing that returns a distribution. From LINEAGE.md section 6.5: no KinOf(tokenID), no
 127// IsKin(a, b), no LineOf(tokenID), no LineCount(line), no KinCount and nothing that returns a
 128// degree, no sort/filter/gate of any kind on a line, a relation or a relative, and NO ENTRYPOINT
 129// ANYWHERE IN ANY REALM THAT BEHAVES DIFFERENTLY FOR TWO RELATED GNOMES THAN FOR TWO STRANGERS.
 130// Added by v7, because it is the first realm where they become reachable: no CareThreshold /
 131// CareScore / Band / PointsToThreshold or any fraction of them; no SecretCount / SecretCensus /
 132// HoldersOfSecret / IsFirstSeventeen and nothing exposing secretsDrawnMask; no Label / Archetype;
 133// no TraitsMatching or any filter or predicate; no unique-in-batch marker; AND NO READER,
 134// ANYWHERE, THAT TAKES TWO TOKEN IDS -- that last one is the structural form of all of it,
 135// because a function of two gnomes is the shape every kin-bonus, related-pair and shared-quest
 136// feature needs. See the shape rule stated once above the trait readers.
 137//
 138// THE INVARIANT THOSE PROTECT, STATED ONCE: NO FUNCTION IN v7 BRANCHES ON A TRAIT, A STAT, A
 139// LINEAGE EDGE OR A SECRET. Not in a guard, not in a loop condition, not in a fee calculation,
 140// not in an eligibility check. Trait data is read by this realm's own code in exactly two
 141// places, and both are inside the draw writing itself down: drawRoll/assertRoll, and
 142// secretWeightsFor's one comparison of boast/fear against the secret target table, which
 143// produces a secret id and nothing else. TestTwoGnomesWithOppositeTraitsAreTreatedIdentically is
 144// that invariant as a test a CI run can check rather than a promise a reviewer has to take.
 145//
 146// THE MIGRATION QUESTION, ANSWERED ONCE SO NOBODY RE-OPENS IT: v5's one gnome STAYS ON v5 and
 147// v4's two stay on v4. No migration, ever -- the identical answer BATCH-ONE.md section 0 gave
 148// one version earlier, for the identical reason: v5 is immutable, has no transfer-in path, and
 149// nothing in v7 can reach it. BATCH ONE IS THE SIXTY GNOMES ON THIS REALM AND NOTHING ELSE; the
 150// 60 is sixty, not fifty-nine plus the one already minted. v5's gnome holds no axis, no
 151// dominance order, no stat line, no secret, no sign, no threshold, no batch index and no kinship
 152// edge -- not because anything was withheld but because V5 HAS NO SUCH FIELDS, so it cannot be a
 153// member of a batch whose defining properties did not exist when it was minted. Nothing in this
 154// file knows v5 exists: not a field, not an import. No cross-realm per-wallet cap (MINT.md
 155// section 6 already settles it -- "a single wallet may hold a v4 gnome and any number of v5
 156// gnomes at once" -- and importing v5 would make v7 undeployable anywhere v5 is not live), no
 157// batch-index offset, no legacy-holder benefit (if the owner wants the v5 holder on the v7
 158// allowlist, that is one AdminAllow call and a curation decision), no cross-realm name
 159// uniqueness (impossible, and already true between v4 and v5), no cumulative TotalSupply, and no
 160// deprecation pointer in v5 (it is immutable). THE ONLY THING v7 INHERITS THAT IS NOT SOURCE
 161// CODE IS THE NAMESPACE.
 162//
 163// AND ONE FACT THAT CHANGES THE SHAPE OF THIS VERSION: v7 SUPERSEDES A REALM THAT NEVER CAME UP.
 164// garden/v6 is written, audited, passing its tests and NOT ON CHAIN -- vm/qpkgmeta_json answers
 165// "status":"inert", three paid addpkg attempts produced nothing retrievable, and a five-line
 166// probe realm with no imports failed identically, which is what proves it is the chain and not
 167// the code (TESTNET.md:530-582). So v6's two fixes have never been live for anybody: SetName and
 168// SettledRound arrive for the first time here, and section 1.1's "byte-for-byte" is a claim about
 169// SOURCE, not about continuity of service. Before attempting to deploy this larger realm, re-run
 170// the five-line probe first -- and remember the lesson that file draws: "OK! from gnokey,
 171// success:true in the block, and a debited balance are together still not evidence that a deploy
 172// happened. Only reading the package back is."
 173//
 174// ============================================================================================
 175// WHAT v6 IS: v5 PLUS EXACTLY TWO FIXES, AND NOTHING ELSE. READ THIS SECOND, AND READ IT WHOLE.
 176// IT IS THE RECORD OF THE AUDITED BASE THIS FILE IS A COPY OF, AND EVERY WORD OF IT STILL
 177// APPLIES EXCEPT WHERE THE TEN CHANGES ABOVE SAY OTHERWISE.
 178// ============================================================================================
 179//
 180// garden/v5 is DEPLOYED AND LIVE on the onyx testnet. v6 exists because a review on 2026-10-16
 181// found a hole that the original design, the test suite and the adversarial security review
 182// (ATTACK-REVIEW.md, eight attack goals) all missed -- not a vulnerability, a MISSING FEATURE
 183// that contradicts something the owner asked for in his own words. This file is a DELIBERATELY
 184// NARROW, SURGICAL COPY of v5. Every line of v5 that is not one of the two fixes below is
 185// carried forward byte-for-byte, including every comment recording why something is shaped as
 186// it is and the three real defects mutation testing found and fixed. v5 survived a security
 187// audit; every unrelated change made here would be a change that has not been audited, so none
 188// was made. If you are about to "also improve" something while reading this file: don't. Write
 189// a v7.
 190//
 191// FIX 1 -- A v5 GNOME CANNOT HAVE A NAME, AND THE GAME NEEDS ONE.
 192//
 193//	v5's gnome struct has no name field, no setter, and `character` -- the only identity string
 194//	it carries -- is "" for every gnome outside the three capped one-of-ones. Confirmed on the
 195//	LIVE CHAIN, not inferred: `GnomeByToken("0000002")` on the deployed v5 returns
 196//	character "". So 21 of Batch One's 24 gnomes would have been permanently anonymous, with no
 197//	way ever to fix it, because the realm is immutable.
 198//
 199//	That directly contradicts the owner's own instruction that in-game characters "should
 200//	refer to you as your gnome name" -- the adventure's whole NPC dialogue surface is built on
 201//	a name existing. v5's own package doc argued the opposite ("NO PLAYER-CHOSEN NAME... the
 202//	app is free to let an owner caption their own gnome off chain"), and that argument was
 203//	WRONG in one specific, load-bearing way worth recording rather than quietly deleting: an
 204//	off-chain caption is not an identity. It cannot travel with the token through a sale, it
 205//	cannot be read by another realm, it cannot be reserved against impersonation, and the
 206//	adventure realm cannot greet you by it. The reasoning that was right -- that nothing
 207//	should be CHOSEN at mint time -- is preserved exactly: see SetName below.
 208//
 209//	THE SHAPE, AND WHY. Naming is a SEPARATE ACT, AFTER the roll, never part of it. `Claim`
 210//	still takes no arguments and `Settle` still takes no name: the entire point of the mint is
 211//	that you do not choose what you get (MINT.md section 1a/section 2), and nothing here
 212//	weakens that. Naming what you already got is a different act entirely, performed by a
 213//	different function, at a different time, by the token's current owner only.
 214//
 215// FIX 2 -- v5 NEVER STORED THE DRAND ROUND THAT PRODUCED EACH GNOME.
 216//
 217//	v5 emits the settling round in its `Settle` event and then forgets it. "Which round made
 218//	this gnome" was therefore a transaction-log archaeology problem rather than a state read:
 219//	`Provenance` returns adoptedAt/acquiredAt/saleCount and there was no getter for the round
 220//	at all. Since "provably unrigged" is this project's headline claim (ECONOMY.md section 1),
 221//	a holder being unable to verify their own gnome with one query was a gap between what the
 222//	project says and what the chain can answer. v6 stores the round on the gnome itself
 223//	(gnome.settledRound) and exposes it (SettledRound). See settleCore.
 224//
 225// ============================================================================================
 226//
 227// WHAT CHANGES FROM v4, IN ONE TABLE, EACH CITED TO THE DOCUMENT THAT DECIDED IT:
 228//
 229//   - Adopt(name, hat, coat, eyes, nose) is GONE. Nothing is chosen by a caller any more.
 230//     DECISIONS.md 2026-10-15 deprecates the DIY picker as a product direction; MINT.md
 231//     section 1a/section 2 replace it with a roll.
 232//   - An ALLOWLIST gates who may ever claim (MINT.md section 11: "gate issuance, never the
 233//     secondary market"). Admin-managed, additive and removable, never touching the mint
 234//     itself once a claim has spent its roll.
 235//   - ONE ROLL PER WALLET, FOREVER, enforced independently of whether the resulting gnome is
 236//     still held (MINT.md section 1a/section 2.3). hasRolled is permanent; nothing in this file
 237//     clears it, and selling the gnome does not refund the roll.
 238//   - FREE. Claim refuses coins exactly as v4's Adopt refuses them (MINT.md section 1a/2.5,
 239//     GAMES.md rule 6: "random when earned, exact when bought... free: random is allowed").
 240//   - THE ROLL IS TWO-PHASE, SETTLED BY DRAND (MINT.md section 10, the load-bearing finding of
 241//     this whole file). A single-transaction roll is peekable two ways, verified against
 242//     gno.land's own source and binary rather than assumed from Ethereum's model: vm/qeval can
 243//     call a crossing function for free with no signature, and `gnokey maketx call -simulate
 244//     only` previews a real signed call against the last committed block with no gas and no
 245//     broadcast. Neither tool is the cause; the cause is that gno.land exposes NO on-chain
 246//     entropy at all -- chain/runtime has ChainHeight and nothing else, math/rand is PCG seeded
 247//     (0,0) on every node forever, and there is no shipped VRF -- so any single-transaction
 248//     formula, however it is dressed up, is something a caller can evaluate offline for every
 249//     block it might land in before ever deciding to broadcast. The fix MINT.md section 10.5
 250//     settles on, and this file builds, is a commit/reveal split across this project's own
 251//     deployed drand verifier: Claim spends the one-time roll and pins a ROUND THAT DOES NOT
 252//     EXIST YET; Settle, permissionless, computes the result once that round is actually
 253//     published. One player signature, not two -- the player signs Claim only.
 254//   - PARTS FROM A WEIGHTED TABLE (ECONOMY.md section 3), not a player's choice, with three
 255//     named one-of-ones (Hob, Thistle, Wick, Tally, Glim, Vigil, Spruce, Yonder -- BATCH-ONE.md
 256//     section 7; three until v7 change 10 made it eight) capped at one each.
 257//   - COUNTED RARITY. Every part's population is tracked on chain and readable by anyone --
 258//     MINT.md section 13's "nine gnomes in the world have this nose" has to be a chain fact,
 259//     and EyesCount/NoseCount/HatCount/CoatCount below are that fact, not an app claim layered
 260//     on top.
 261//   - MINT ONE, HOLD MANY, PLAY ONE (MINT.md section 2a). Buy's one-gnome-per-address refusal is
 262//     GONE -- deliberately, and only from Buy, never from Claim (see Claim's hasRolled gate,
 263//     which is the cap that still matters). An address may hold any number of gnomes; exactly
 264//     one is ever AWAKE, tracked by activeToken, and only the awake one can ever gain a cared
 265//     day, a streak, a level or XP. SaveDay and ChooseType resolve through activeToken, never
 266//     through "the" gnome for an address, because there no longer is one.
 267//   - SWITCHING (Wake) is free, at most once per UTC day (MINT.md section 2a), the same
 268//     day-boundary discipline SaveDay's own dailyCareDay already uses.
 269//   - THE SECONDARY MARKET IS OPEN: Buy/List/Cancel carry over v4's shape and fee (MINT.md
 270//     section 11), with no allowlist on buying and no hold cap.
 271//   - THE LOCK: the eight named one-of-ones cannot be listed until they have lockCaredDays
 272//     cared days; every ordinary gnome trades from day one (MINT.md section 12/13, "Option A" --
 273//     lock only the named one-of-ones). SETTLED 2026-10-19 AT 14, down from that section's
 274//     unconfirmed recommendation of 30; reasoning at lockCaredDays itself. It is still one named
 275//     constant and nothing else in this file assumes its value -- verified by AUDIT-03, which
 276//     also confirms maxListingDays' own 30 was never coupled to it.
 277//     A gnome's own caredDayCount never decreases and is not reset by Buy, so once
 278//     a named gnome unlocks it stays unlocked through every future sale.
 279//   - A GENESIS GNOME, minted to this realm's own address at deploy, non-transferable, never
 280//     listable, separate from the 60-gnome batch (DECISIONS.md 2026-10-15: "the genesis gnome
 281//     belongs to nobody, permanently, on purpose"). ITS NAME IS SPRIG, settled 2026-10-19 --
 282//     see genesisGnomeName below for why, and note it is load-bearing in FOUR places, not one:
 283//     the constant, reservedNameKeys, the gnome's own `character` slug, and characterOwner.
 284//
 285// WHAT IS DELIBERATELY OUT OF SCOPE FOR THIS FILE, EACH A SCOPING DECISION MADE BY THIS PASS,
 286// NOT A GAP IN THE BRIEF THAT ASKED FOR IT. Recorded here in the open the same way MINT.md
 287// records its own open questions, so a future reader finds the reasoning in one hop:
 288//
 289//   - NO SetRule/FreezeRules TUNABLE ADMIN SURFACE. v4's ruleBand/SetRule/FreezeRules machinery
 290//     is real and proven, but every care-mechanic number below is a fixed constant at v4's own
 291//     deploy defaults instead. The brief this file was written against lists eleven concrete
 292//     requirements and none of them is "tunable rules"; adding v4's whole admin-ratchet surface
 293//     on top of everything already new here would have roughly doubled this file's size and
 294//     its audit surface for a feature nobody asked for in this pass. A FUTURE VERSION can add
 295//     it back following v4's own pattern exactly if the owner wants it. (v5 said "a v6" here;
 296//     v6 exists now and deliberately did not, for the same reason it added nothing else.)
 297//   - NO NAME AT MINT TIME. This entry REPLACES v5's own "NO PLAYER-CHOSEN NAME" entry, which
 298//     is the thing v6 exists to correct -- see the "WHAT v6 IS" section at the top of this file
 299//     for the full account of what that argument got right and what it got wrong. What survives
 300//     from it, unchanged and non-negotiable: NOTHING IS CHOSEN BY A CALLER AT MINT TIME,
 301//     INCLUDING A NAME. `Claim` takes no arguments; `Settle` takes no name. v4's
 302//     validNameRune/validNameCharset/hasVisibleNameContent machinery (three audit rounds' worth,
 303//     per v4's own comment) is carried back into this file WHOLE, not re-derived, because the
 304//     naming act moved rather than disappeared -- it is now SetName, callable only by the
 305//     token's current owner, only after the roll has already irrevocably decided what they got.
 306//   - NO OpenBatch ADMIN SURFACE. MINT.md section 6 floats an owner-callable OpenBatch for
 307//     adding future batches' rosters; the task this file was built against names a single
 308//     60-gnome batch (BATCH-ONE.md, at DECISIONS.md 2026-10-17's size) and does not ask for a
 309//     mechanism to open a second one. The
 310//     batch here is sized once, at deploy, by the batchCap/numNamedEditions constants below. A
 311//     FUTURE VERSION is the natural place for OpenBatch if a second batch is ever wanted, the
 312//     same way every other irreversible-shape decision in this project has gotten its own
 313//     version rather than a retrofit. (v5 said "a garden/v6 is the natural place"; v6 exists
 314//     now and deliberately did NOT take that opportunity -- it is two fixes and nothing else.)
 315//
 316// A REAL FLAW FOUND AND FIXED WHILE DESIGNING Settle, RECORDED HERE RATHER THAN QUIETLY FIXED
 317// AND FORGOTTEN -- the same discipline MINT.md section 10 models for its own single-transaction
 318// finding. An earlier version of this design picked the named one-of-ones by drawing
 319// "without replacement" from a shrinking pool: remainingTotal = batchCap - settledCount,
 320// remainingNamed = 3 - namedAssignedCount, and a claimant's pick = seed % remainingTotal. THAT
 321// IS PEEKABLE AND GAMEABLE, even though the seed itself is honestly drand-derived and
 322// unforgeable: because remainingTotal only ever shrinks as OTHER claimants settle, and Settle is
 323// permissionless and callable at any later time, a claimant whose seed is already fixed (the
 324// moment their pinned round publishes) can simply precompute, for every possible remainingTotal
 325// from 1 to batchCap, whether that value would win them a named edition -- and then wait,
 326// costing nothing, until the rest of the batch happens to settle into a state that does. The fix
 327// below removes every dependency on shrinking, observable, time-varying state from the decision:
 328// a claimant's own named-edition pick is `seed % namedPickRange` (`seed % batchCap` until v7
 329// change 9 moved the modulus off the batch size on 2026-10-18 -- a different constant, the same
 330// and only load-bearing property), against a modulus that is a
 331// COMPILE-TIME CONSTANT, not a moving count, so there is nothing left to wait for -- the only
 332// residual order-dependence is which of possibly several colliding claimants (bad luck, not
 333// strategy: nobody can change their own pick by choosing when to settle) gets a named edition
 334// that more than one of them rolled, and that is resolved by whichever Settle call lands on
 335// chain first.
 336//
 337// A SECOND AUDIT PASS NAMED THIS PRECISELY: "settling promptly", said above and in an earlier
 338// version of Settle's own comment, OVERSTATES what the residual actually rewards. It is not
 339// "whoever claimed first" or "whoever cared most" -- pendingClaim.claimedAt is never consulted
 340// by the tie-break, and Settle is permissionless, so a bystander (a searcher, a bribed relayer,
 341// a claimant's own bot) can win a collision for whichever side pays for transaction-ordering
 342// priority, regardless of who rolled or claimed first. Considered and rejected: tie-breaking on
 343// claimedAt instead. It sounds like the honest fix, but it is not reachable without a worse
 344// trade-off -- Settle finalizes irrevocably (it mints a real token to a real owner, who may
 345// already have acted on it), so honoring an earlier claimedAt would mean either refusing to
 346// finalize ANY collision candidate until every possible earlier claimant's round has published
 347// and settled (reintroducing exactly the wait-and-see window section 10 and this fix exist to
 348// close, now around a deadline instead of a pool size) or reaching back to UNDO an already-
 349// settled mint (which nothing in this realm, or any grc721-based realm in this project, has ever
 350// claimed to be able to do, and which would be a strictly worse property than the one being
 351// fixed). What actually holds, stated precisely rather than aspirationally: nobody can improve
 352// THEIR OWN odds by choosing when to settle (the property section 10 is about, and the property
 353// this fix actually guarantees); the residual is an ordinary transaction-ordering race over a
 354// LOW-PROBABILITY, LOW-STAKES event (three slots in a batch of 24, decided among whichever
 355// handful of claimants happen to collide -- WRITTEN BEFORE CHANGES 1, 9 AND 10, and "low
 356// probability" and "a handful" are the two words that stopped being true; see the paragraph
 357// below), the same shape of race every permissionless action on
 358// every chain is already subject to, not a new hole this design opened. See Settle's own comment
 359// for the arithmetic. v7 CHANGES 9 AND 10 (both 2026-10-18) MAKE THAT RACE MORE FREQUENT AND SAY
 360// SO: at namedPickRange = 10 roughly 18 of 60 claimants held a named-pointing pick for three slots,
 361// and at change 10's eight slots it is 48 of 60 (60 * 8/10) chasing eight, so collisions are now
 362// the NORMAL CASE rather than an unlucky one -- about 40 claimants draw a named pick and fall
 363// through to an ordinary draw. The race is unchanged in SHAPE -- first settle takes it, nobody can
 364// move his own pick -- and it rewards promptness, not lateness. The honest statement of that cost,
 365// and the reason the rate was raised anyway, are at namedPickRange.
 366//
 367// SETTLE CAN NOW PAY FOR ITS OWN BEACON RELAY, OWNER-APPROVED 2026-10-15, THE DAY AFTER THIS FILE
 368// WAS FIRST WRITTEN. The two-phase roll above depends on beacon/v2 actually having the pinned
 369// round's drand signature by the time Settle runs, and until this change that was somebody
 370// else's problem: a relay has to happen, by a human or a bot, running outside this realm and
 371// outside this project's control, or a claimant who has already spent their one-and-only roll is
 372// stuck mid-mint through no fault of their own -- found live, not hypothetically, while building
 373// this file: the project's own beacon was observed ~2 days stale. The fix the owner asked for
 374// ("can we have the mint transaction pay the beacon though") is `SettleWithSignature` below: the
 375// player's own app fetches the pinned round's signature from drand's public API (the same
 376// `https://api.drand.sh/<chain>/public/<round>` this file's own test fixtures are fetched from)
 377// and passes it straight through; Settle relays it itself, in the same transaction, before
 378// computing the result. One transaction, and the player pays its gas -- TO THE NETWORK, same as
 379// any other transaction's gas, NOT to this project, so it never touches the free-mint promise
 380// (MINT.md section 14: "gas is still a payment, just not to us... the operator takes no stake").
 381// Three things this buys, in the owner's own order of importance:
 382//
 383//   - IT COSTS THE PROJECT NOTHING. The alternative to a player-paid relay is an operator-run one
 384//     -- a cron job or a bot calling beacon.Publish on a schedule, which is real recurring GNOT
 385//     spend with no revenue to offset it (MINT.md section 14 forecloses an unlock fee, batch one
 386//     is free end to end). This change deletes that cost rather than budgeting for it.
 387//   - A PLAYER CAN NEVER BE BLOCKED BY THIS PROJECT'S OWN HOUSEKEEPING. The stuck-mid-mint state
 388//     above is now unreachable for anyone willing to fetch their own round's signature: their own
 389//     Settle call makes the round available, with no dependency on this project, a third-party
 390//     relay, or anyone else's goodwill.
 391//   - THE BEACON GETS FRESHER AS A SIDE EFFECT, FOR EVERYONE ELSE USING IT. beacon/v2 is a shared,
 392//     permissionless, on-demand record (see its own package doc) -- a round this realm's own
 393//     claimants cause to be published is published for every other consumer of that beacon too,
 394//     not just for this realm.
 395//
 396// THREE THINGS WORKED OUT BY QUERYING THE LIVE CHAIN, NOT ASSUMED, because this is new
 397// cross-realm write surface and this project's own history (the SPLASH beacon migration,
 398// MEMORY.md) is built from exactly the kind of interrealm detail that gets guessed wrong:
 399//
 400//  1. WHAT beacon.Publish DOES ON AN ALREADY-PUBLISHED ROUND. Fetched byte-for-byte from the live
 401//     deployed realm itself (gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2) via
 402//     the public onyx-1 RPC's `vm/qfile` query, 2026-10-15, and diffed identical against this
 403//     project's own vendored copy at realms/_vendor/beacon/v2/beacon.gno -- it PANICS, it does not
 404//     no-op: `func Publish(cur realm, round int64, sigHex string) { if Published(round) { panic
 405//     ("beacon: round already published") } ... }`. SettleWithSignature therefore checks
 406//     `beacon.Published` itself BEFORE ever calling Publish, exactly once, and never calls Publish
 407//     at all if the round is already there -- calling it unconditionally would make this function
 408//     fail every time a relay (another claimant's own SettleWithSignature, or anyone else) beat it
 409//     to the punch, which given Settle's own permissionless, callable-by-anyone design is an
 410//     ordinary and expected race, not an edge case.
 411//  2. THAT Publish IS REACHABLE CROSS-REALM AS `beacon.Publish(cross(cur), round, sigHex)`, and
 412//     that it carries no caller-identity check at all for this function to trip over. Confirmed
 413//     two ways: the live source pulled in point 1 shows Publish's body never reads `cur` or calls
 414//     `cur.Previous()` -- it is permissionless by whoever relays, not merely by whichever address
 415//     -- and this exact call shape is already proven working, not merely plausible, by this
 416//     file's OWN test harness: garden_test.gno's `publishAs` helper has called
 417//     `beacon.Publish(cross(cur), round, sigHex)` from this package since the day this file was
 418//     written, and every test that calls it passes. SettleWithSignature's own call is the
 419//     identical shape with `cur` being Settle's own realm parameter instead of a test helper's.
 420//     (`cur.Previous()` inside beacon.Publish would resolve to THIS realm, garden/v5, when called
 421//     this way rather than to the human wallet that called Settle -- a real interrealm fact worth
 422//     recording even though Publish does not currently branch on it: if beacon/v2 ever grows a
 423//     caller-identity check, re-read this paragraph before assuming Settle's relay still works.)
 424//  3. GAS. Measured on this toolchain with r/splash/beacon/zz_gas_test.gno's own method
 425//     (`gno test -run TestZZGas -print-runtime-metrics=true -v`, net of this package's own fixed
 426//     per-test-call overhead): a bare Settle today costs ~6.2M gas; beacon.Publish alone costs
 427//     ~18.5M gas on THIS realm's measurement (the same order of magnitude as beacon/v2's own
 428//     documented 16,970,375, re-measured rather than quoted because toolchain and realm storage
 429//     both shift it). SettleWithSignature's worst case -- the round not yet published at all --
 430//     is therefore on the order of ~25M gas total, call it 1% of the 3e9 block gas limit and a
 431//     few hundredths of a GNOT at this chain's gas price. Reasonable: it is roughly four times a
 432//     bare Settle, not an order of magnitude more, it still leaves room for well over a hundred
 433//     such settlements in a single block, and it replaces a cost this project would otherwise have
 434//     paid itself, recurringly, forever, to keep the beacon fresh by some other means.
 435//
 436// THE SHAPE: TWO ENTRY POINTS, ONE SHARED CORE, NOT ONE FUNCTION WITH AN OPTIONAL SIGNATURE.
 437// `Settle(cur realm, claimant address)` is UNCHANGED in behaviour and UNCHANGED in signature --
 438// every existing test and every existing caller that already knows (or does not care) whether the
 439// round is published keeps working exactly as before, at exactly its old gas cost, with no
 440// signature to fetch or supply. `SettleWithSignature(cur realm, claimant address, sigHex string)`
 441// is new, and is for a caller that HAS a signature and wants its transaction to publish the round
 442// if nobody else already has. A Gno realm's exported functions take a fixed argument list -- there
 443// is no overloading, and an optional/variadic last argument would still force every caller,
 444// including every test and the app's own simpler "it's already published" path, to pass an empty
 445// string they have no use for, for a case (publishing) that is the EXCEPTION, not the rule, since
 446// most settlements land after some earlier claimant or relay has already published the shared
 447// round. Two named entry points make the exception visible in the function name instead of buried
 448// in an argument, and cost nothing extra: both share one unexported `settleCore`, so there is
 449// exactly one copy of the draw-and-mint logic, not two drifting in parallel. And it is SAFE for
 450// either one to be called in either actual on-chain state: Settle still refuses cleanly, with its
 451// original message, if the round genuinely is not published yet; SettleWithSignature still
 452// succeeds, with its supplied signature simply going unused, if the round turns out to already be
 453// published by the time it runs (point 1's race) -- neither caller has to know in advance which
 454// state they are in, they only have to know whether they personally HAVE a signature to offer.
 455package garden
 456
 457import (
 458	"crypto/sha256" // v7 change 8: seedFor's hash. See seedFor and the "WHY FNV-1a HAD TO GO" block.
 459	"strconv"
 460	"strings"
 461	"time"
 462	"unicode"      // v6 fix 1: v4's name charset allowlist, carried back whole. See validNameRune.
 463	"unicode/utf8" // v6 fix 1: rune counting, never len(). See validNameLength.
 464
 465	"gno.land/p/nt/avl/v0"
 466	"gno.land/p/nt/grc721/v0"
 467	"gno.land/p/nt/seqid/v0"
 468
 469	"chain"
 470	"chain/banker"
 471	"chain/runtime/unsafe"
 472
 473	beacon "gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/beacon/v2"
 474)
 475
 476// ============================================================================
 477// THE THREE SALE CONSTANTS. CARRIED OVER, UNCHANGED, FROM garden/v4's 2026-10-11 DECISION, AS
 478// PLACEHOLDERS FOR THIS NEW REALM. Nobody has asked the owner to re-confirm these specifically
 479// for v5 -- re-decide or re-confirm them before ever deploying this file; see gnomod.toml's own
 480// checklist item 0. init() below still gates them exactly as v4's assertFeeConfig does, so an
 481// edit to a value that cannot work fails at deploy rather than succeeding silently forever.
 482// ============================================================================
 483const (
 484	saleFeeRecipient  = "g1ylpe9juk8cafgf9rt09wzuz85ug2lyfk5metyv" // gnotchi-opponent, v4's choice
 485	saleFeeRateBps    = int64(500)                                 // 5% of the declared price
 486	saleFeeFloorUgnot = int64(1_000_000)                           // 1 GNOT flat minimum per sale
 487)
 488
 489// ============================================================================
 490// Constants that are NOT tunable, and never will be: see the package doc's "out of scope"
 491// section for why v5 has no SetRule surface at all.
 492// ============================================================================
 493
 494const (
 495	collectionName   = "Gnotchi gnomes"
 496	collectionSymbol = "GNOME"
 497
 498	// adminAddress is the only address AdminAllow/AdminDisallow will accept. Compiled in, with
 499	// no transfer function, for the same reason v4 has none: an admin-transfer path is a second
 500	// authority path, and a lost admin key merely freezes the allowlist, which is recoverable by
 501	// redeploying a v6 -- not a disaster.
 502	//
 503	// THIS IS THE ONYX TESTNET DEPLOY KEY `gnotchi-deploy`, which is also the namespace this
 504	// realm deploys under (see gnomod.toml) and the only key on this host with enough GNOT to
 505	// pay for the admin calls -- AdminAllow and Settle both cost fees, and v4's old key
 506	// (g1r4aw43..., the retired splash-deploy) is down to dust, so compiling that address in
 507	// here would have produced an admin that could not afford to act. Correct for onyx, wrong
 508	// for anywhere real. Change it before deploying anywhere that matters; see gnomod.toml.
 509	adminAddress = "g1nq99m3h9ed7vqve6ajpr3qwy2ls252m8jnundm"
 510
 511	// numEyes / numNoses / the hat and coat catalogues: THE SAME FOUR-DIMENSIONAL FACE SPACE
 512	// garden/v4 uses (SPRITES.md's Phase 1: 10 eyes x 8 noses x 4 hats x 4 coats = 1,280 looks).
 513	// Nothing here is a new catalogue; eyesWeights/noseWeights below are a weighted DRAW over
 514	// the same ids v4 already validates, per ECONOMY.md section 3 and MINT.md section 13's
 515	// counted-rarity decision ("no tiers... rarity becomes population frequency counted on
 516	// chain"). hat and coat are drawn UNIFORMLY (25% each): ECONOMY.md section 3 only specifies
 517	// weights for the "permanent pair" (eyes, nose), and BATCH-ONE.md section 4a says so
 518	// explicitly -- "assigning the section 3 percentages to hat and coat as well... is real work
 519	// this document does not do... flagged here as the next thing to specify". A uniform draw is
 520	// the honest placeholder until that work happens, not a guess dressed up as one.
 521	numEyes  = int64(10)
 522	numNoses = int64(8)
 523
 524	// hourSeconds/daySeconds: unchanged from v4.
 525	hourSeconds = int64(3600)
 526	daySeconds  = int64(86400)
 527
 528	// ---- care-mechanic constants, fixed at v4's own deploy DEFAULTS (v4's ruleBands table).
 529	// See the package doc: v5 deliberately carries no tunable-rules admin surface.
 530	numPlots        = 3        // acorns.plots
 531	plantCost       = int64(1) // acorns.plantCost
 532	feedCost        = int64(1) // acorns.feedCost
 533	acornsStart     = int64(10)
 534	meterMax        = int64(10)
 535	patCheer        = int64(3)
 536	hungryAtOrBelow = int64(2)
 537
 538	growHours           = int64(24)
 539	patCooldownHours    = int64(12)
 540	tinkerCost          = int64(5)
 541	levelBase           = int64(50)
 542	levelStep           = int64(25)
 543	sulkyPenaltyPct     = int64(10)
 544	harvestPerPlot      = int64(3)
 545	feedBelly           = int64(4)
 546	bellyDropEveryHours = int64(3)
 547	cheerDropEveryHours = int64(6)
 548	dailyCareCap        = int64(60)
 549
 550	// acornReserve: v4's fix for the acorn softlock (AUDIT-DEPLOYED.md G9), carried over
 551	// unchanged. See v4's own constant for the full argument; the short version is that a
 552	// SaveDay batch that spends acorns may never end with fewer than this and nothing planted.
 553	acornReserve = plantCost + feedCost
 554
 555	happyBonusPct    = int64(10)
 556	happyWindowHours = int64(2)
 557	sulkyAfterHours  = int64(72)
 558	maxLevel         = int64(20)
 559	typeAtLevel      = int64(5)
 560
 561	xpPat            = int64(5)
 562	xpFeed           = int64(5)
 563	xpHarvestPerPlot = int64(10)
 564	xpTinkerTask     = int64(15)
 565
 566	maxActionsPerCall = 32
 567	maxBackfillHours  = int64(48)
 568	caredWindowDays   = uint(64)
 569
 570	maxGnomesPageSize   = 100
 571	maxListingsPageSize = 100
 572
 573	// maxKinEdgesPageSize: v7, the same shape and the same number as the two above, for
 574	// KinEdgesPage. The whole table is 84 rows, so one page of 100 returns all of it -- the
 575	// cap exists to keep the enumerator's shape identical to its two siblings rather than
 576	// because 84 needs bounding.
 577	maxKinEdgesPageSize = 100
 578
 579	// ---- the sale. Shape constants, unchanged from v4.
 580	ugnotDenom     = "ugnot"
 581	maxPriceUgnot  = int64(1_000_000_000_000_000)
 582	maxFeeRateBps  = int64(10_000)
 583	maxListingDays = int64(30)
 584
 585	moodHungry  = "hungry"
 586	moodSulky   = "sulky"
 587	moodHappy   = "happy"
 588	moodContent = "content"
 589
 590	actionPat     = "pat"
 591	actionFeed    = "feed"
 592	actionPlant   = "plant"
 593	actionHarvest = "harvest"
 594	actionTinker  = "tinker"
 595
 596	typeGardener = "gardener"
 597	typeExplorer = "explorer"
 598	typeTinkerer = "tinkerer"
 599
 600	// ============================================================================
 601	// THE MINT: allowlist, one roll per wallet, the batch, the lock, the drand pin. Every
 602	// number below is a single named constant, each trivially changeable, per the brief's own
 603	// instruction for the lock specifically and applied here to the pin offset too.
 604	// ============================================================================
 605
 606	// batchCap: BATCH-ONE.md's own roster, AT THE SIZE DECISIONS.md SETTLED ON 2026-10-17
 607	// ("Batch One becomes 60", superseding "Batch One stays 24" from earlier the same day) --
 608	// 60 gnomes, total, forever, this batch. 52 ordinary plus eight named one-of-ones
 609	// (numNamedEditions below -- three until v7 change 10, which is also the change that
 610	// explains why the split is worth writing out: three of 24 was 12.5% of a batch and three of
 611	// 60 was 5%, so change 1 moved this number and silently moved a rarity nobody was discussing.
 612	// Eight of 60 is 13.3%). Closed the instant mintedCount reaches this; nothing in this file
 613	// can ever raise it (no OpenBatch -- see the package doc's "out of scope" section).
 614	//
 615	// THIS IS v7 CHANGE 1 AND IT IS ONE CONSTANT. Everything that reads it -- Claim's cap gate,
 616	// BatchStatus's third return, the kinship table's 1..60 key space and OwnerTokens' unpaginated
 617	// bound -- reads it from here and nothing in this file hardcodes 24 or 60 beside it. THE
 618	// NAMED-EDITION PICK NO LONGER READS IT (v7 change 9, 2026-10-18): its modulus is
 619	// namedPickRange, so changing the batch size no longer silently changes the odds on any
 620	// edition. See namedPickRange for why that coupling was accidental and had to go. THE ROSTER
 621	// SIZE IS STILL NOT INDEPENDENT OF THIS NUMBER AS A DESIGN QUESTION, though -- change 10 is
 622	// the record of batchCap moving and numNamedEditions needing to follow it by hand, which is a
 623	// judgement about what fraction of a batch should be special, not a coupling in the code.
 624	batchCap = int64(60)
 625
 626	// numNamedEditions: THE SIZE OF THE NAMED ROSTER, AND v7 CHANGE 10 (2026-10-18). Hob, Thistle,
 627	// Wick, Tally, Glim, Vigil, Spruce and Yonder (namedEditionOrder below), cap ONE EACH. "Cap 1"
 628	// is not a different mechanism from any other cap, MINT.md section 3's own point -- it is the
 629	// same characterOwner.Has check any other population cap would use, just at the number one.
 630	//
 631	// THREE UNTIL 2026-10-18, when the owner said "If we are doing 60 there should be more
 632	// specials than 3." The arithmetic behind him: three of 24 was 12.5% of the batch, and v7
 633	// change 1 took the batch to 60 without touching the roster, which cut it to 5% as a side
 634	// effect of a decision about batch size. Eight of 60 is 13.3% -- the proportion restored, not
 635	// a new one invented. The five new entries are EXISTING, ALREADY VALIDATED ART renamed, exactly
 636	// as Hob (the gilded gnome) and Thistle (the Viking) were; BATCH-ONE.md section 7 names each
 637	// one, says which character it is and justifies the name. No art was drawn for change 10.
 638	//
 639	// EDITING THIS NUMBER IS FOUR EDITS, NOT ONE, AND THE FOURTH IS THE ONE A REVIEWER MISSES.
 640	// Three are the tables that must stay the same length and in the same order:
 641	// namedEditionOrder, namedEditionDisplayNames and reservedNameKeys (which is
 642	// [numNamedEditions + 1] -- the extra slot is the genesis gnome). The FOURTH IS PROSE THAT
 643	// SAYS THE COUNT OUT LOUD IN A PANIC MESSAGE: List refuses an early sale with "... is one of
 644	// the eight named one-of-ones and cannot be listed until ...", and the test suite's lockedMsg
 645	// helper mirrors that string byte-for-byte, so the count appears in two more places than the
 646	// tables. gnomod.toml item (4) names the roster as well and is a build input, not source.
 647	// TestReservedNamesCoverEveryNamedEdition and TestNamedEditionDisplayNamesPairWithTheirCanonicalKeys
 648	// pin the tables against each other and against BATCH-ONE.md's own spelling.
 649	//
 650	// AND THIS NUMBER MAY NOT GROW AGAIN WITHOUT RAISING namedPickRange: the pick is
 651	// `seed % namedPickRange` and an edition at an index >= namedPickRange can never be drawn at
 652	// all. 10 >= 8 holds with two residues of headroom; at eleven editions an edition would be
 653	// unmintable and the realm immutable. TestNamedEditionOddsAreOneInTen asserts the inequality
 654	// directly so that mistake fails a test rather than shipping.
 655	numNamedEditions = 8
 656
 657	// namedPickRange: THE MODULUS OF THE NAMED-EDITION PICK, AND v7 CHANGE 9 (2026-10-18).
 658	// settleCore draws namedPick = seedFor(round, claimant, "slot") % namedPickRange and takes the
 659	// edition at that index when the result is under numNamedEditions -- so every claimant has a
 660	// 1-in-10 shot at each specific one of the eight editions. The owner's instruction, in his
 661	// words: "make the last few have them. The rates have to come out during a mint." He wants all
 662	// of them to actually exist.
 663	//
 664	// THIS CONSTANT DID NOT MOVE IN v7 CHANGE 10 AND MUST NOT: change 10 raised the ROSTER from
 665	// three to eight at these same owner-approved odds. A specific edition is a 1-in-10 shot now
 666	// exactly as it was at three, because the odds on a specific edition are what this constant
 667	// controls and the roster size is what numNamedEditions controls. What the two compose to did
 668	// move, and the table below is recomputed for eight rather than edited in place.
 669	//
 670	// THE ARITHMETIC, REPRODUCED RATHER THAN QUOTED, because this constant exists for nothing
 671	// except what it does to these numbers. Each claimant draws independently, so for one specific
 672	// edition P(never drawn by n claimants) = (1 - 1/namedPickRange)^n, and P(at least one of the
 673	// k editions absent) = 1 - (1 - that)^k, at k = numNamedEditions = 8:
 674	//
 675	//                                   miss one specific       at least one of eight missing
 676	//   1-in-60 (v5/v6), 60 claimants   (59/60)^60 = 36.48%     1 - (1-0.36479)^8 = 97.35%
 677	//   1-in-10,         60 claimants   (9/10)^60  =  0.18%     1 - (1-0.0017971)^8 =  1.43%
 678	//   1-in-10,         35 claimants   (9/10)^35  =  2.50%     1 - (1-0.0250316)^8 = 18.36%
 679	//   1-in-10,         30 claimants   (9/10)^30  =  4.24%     1 - (1-0.0423911)^8 = 29.29%
 680	//
 681	// AT THREE EDITIONS THE SAME TWO MIDDLE ROWS WERE 0.54% AND 7.32%; the per-edition column is
 682	// identical and only the composition changed, which is the whole shape of change 10. Expected
 683	// number of the eight that actually mint in a full batch is 8 * (1 - (9/10)^60) = 7.986.
 684	//
 685	// The last two rows are there because a batch that does not sell out is the realistic bad case,
 686	// and the fourth row is the one to quote at anybody planning the allowlist: A BATCH THAT ONLY
 687	// HALF FILLS (30 of 60 claimed) LOSES AN EDITION ALMOST THREE TIMES IN TEN. That is the price
 688	// of eight rather than three and it is not small. It is still better than a FULL batch was at
 689	// 1-in-60 under either roster size. TestNamedEditionOddsAreOneInTen pins the rate against the
 690	// realm's own seedFor, and TestNamedEditionsAtSixtyAreNotGuaranteed pins the residual 1.43%.
 691	//
 692	// WHY THIS IS NOT batchCap, which is what the modulus was until 2026-10-18 and the only reason
 693	// the odds were ever 1-in-60. THAT COUPLING WAS ACCIDENTAL: batchCap was reached for because it
 694	// was the handy compile-time constant to hand, and it made the rarest-asset rate a side effect
 695	// of a batch size decided for entirely unrelated reasons -- 24, then 60 (DECISIONS.md
 696	// 2026-10-17), each change silently moving odds nobody was discussing. The two numbers answer
 697	// different questions: batchCap is HOW MANY GNOMES EXIST, namedPickRange is HOW LIKELY A NAMED
 698	// EDITION IS, and numNamedEditions is HOW MANY THERE ARE TO BE LUCKY ABOUT. A future batch of
 699	// 200 must not quietly make Hob a 1-in-200 shot, so the modulus is its own constant and moving
 700	// any one of the three leaves the others alone -- v7 change 10 is the proof that this
 701	// separation was worth having: the roster tripled in size and this line did not move. What was
 702	// load-bearing about batchCap -- and the only thing this must inherit from it -- is that the
 703	// modulus is a COMPILE-TIME CONSTANT and not a count of what remains; see the package doc's "a
 704	// real flaw found and fixed" section for why that distinction is the whole safety property. Any
 705	// value here must be >= numNamedEditions or an edition becomes unreachable (10 >= 8, which was
 706	// 10 >= 3 before change 10 and now has only two residues of slack -- a NINTH edition would be
 707	// fine, an ELEVENTH would be permanently unmintable), and TestNamedEditionOddsAreOneInTen
 708	// checks that too.
 709	//
 710	// WHY RAISING THE RATE IS SAFE WHERE THE REMOVED GUARANTEE WAS NOT, which is the reason this
 711	// change is a constant rather than a mechanism. A claimant's namedPick is a PURE FUNCTION of
 712	// the pinned drand round and his own address -- fixed before he can act and unchangeable by
 713	// anything he does. WAITING DOES NOT HELP, RUSHING DOES NOT HELP, WATCHING THE COUNTERS DOES
 714	// NOT HELP: THERE IS NOTHING TO POSITION FOR. The forcing rule removed by AUDIT-01 RED-1 fired
 715	// on a SHRINKING PUBLIC OBSERVABLE (slotsLeft = batchCap - settledCount, computable from
 716	// BatchStatus and CharacterOwner), and that is exactly what made it positionable -- a wallet
 717	// that simply never claimed until 57 were settled took an edition with certainty. This keys on
 718	// nothing observable at all: 60 -> 10 changes a probability and introduces no new input, no new
 719	// state, no new branch and no new reader.
 720	//
 721	// AND THE COST, STATED HONESTLY BECAUSE IT IS REAL, AND CHANGE 10 MAKES IT MUCH BIGGER. At
 722	// 1-in-10 with three editions, roughly 18 of 60 claimants (60 * 3/10) held a pick pointing at a
 723	// named edition and only three could have one. AT EIGHT EDITIONS IT IS 48 OF 60 (60 * 8/10) --
 724	// four claimants in five draw a named-pointing pick, eight of them can keep it, and ABOUT FORTY
 725	// LOSE THE RACE and fall through to an ordinary draw. Among claimants whose picks collide,
 726	// WHOEVER SETTLES FIRST TAKES IT: v5's and v6's already-documented transaction-ordering race,
 727	// in the same place and under the same comment, byte-for-byte unchanged -- but it is now the
 728	// NORMAL EXPERIENCE OF A CLAIMANT rather than an unlucky edge case. It rewards PROMPTNESS
 729	// rather than lateness, which is the healthy direction and the exact opposite of what the
 730	// removed rule did. And because the draw is public arithmetic over public inputs, a claimant
 731	// CAN WORK OUT AFTER THE FACT that he was in line for Hob and lost the race -- and after change
 732	// 10, forty claimants per batch can. Accepted twice, with eyes open: the alternative on offer
 733	// at change 9 was a 74% chance that one of three pieces of commissioned art was never minted at
 734	// all, and the alternative at change 10 was leaving five finished, validated characters
 735	// (ARCHETYPES.md section 3's own Legendary line) unminted while the batch grew around them.
 736	//
 737	// THIS IS STILL NOT A GUARANTEE, and the source must not start reading as though it were.
 738	// ~1.43% of full batches, ~18.4% of 35-gnome batches and ~29.3% of half-filled 30-gnome
 739	// batches miss at least one of the eight.
 740	namedPickRange = 10
 741
 742	// lockCaredDays: the eight named one-of-ones cannot be listed until they have this many cared
 743	// days; every ordinary gnome trades from day one. One named constant, and nothing else in this
 744	// file assumes any particular value.
 745	//
 746	// SETTLED 2026-10-19 AT 14, DOWN FROM 30. MINT.md section 12/13's thirty was explicitly a
 747	// RECOMMENDATION -- "Option A... this is a recommendation, not a decision" -- and sat unconfirmed
 748	// through v5, v6 and most of v7. The owner delegated it ("ok whatever you think") after being
 749	// asked three times.
 750	//
 751	// WHY 14 AND NOT 30. The lock exists so a one-of-one cannot be flipped the hour it is minted;
 752	// it does not exist to make the holder prove devotion. Thirty DISTINCT cared days before an
 753	// asset becomes usable is a month of attendance, and the realistic outcome of that is not a
 754	// more committed holder but an abandoned gnome and a dead listing -- the care ledger already
 755	// measures attention, and the resale lock is a speed bump, not a second measure of it.
 756	//
 757	// SAID PRECISELY, because the first draft of this comment said "unbroken attendance" and that
 758	// is a mechanism this realm does not have (AUDIT-03 GREEN-4): the lock counts TOTAL distinct
 759	// cared days, not consecutive ones, so no streak has to be unbroken and a missed day costs a
 760	// day rather than a restart. The floor is still 14 calendar days, because recordCaredDay banks
 761	// at most one distinct UTC day at a time -- and lastCaredDayForOwner means an owner holding
 762	// several gnomes must spend each of those days on THIS one.
 763	// Fourteen is still long enough that flipping is not a strategy and short enough that somebody
 764	// who wants out can get out inside a fortnight.
 765	//
 766	// NOT ZERO, and not "let the market decide": the eight are the scarcest things in the realm and
 767	// a mint-and-flip on day one is exactly the behaviour BATCH-ONE.md's hand-read waitlist exists
 768	// to keep out.
 769	//
 770	// TestTheSellLockIsFourteenCaredDays pins the number itself, because the two behavioural tests
 771	// around List are both written relative to this constant and therefore pass at ANY value --
 772	// verified by mutation on 2026-10-19: setting it to 99 broke nothing.
 773	lockCaredDays = int64(14)
 774
 775	// ============================================================================
 776	// v6 FIX 1: THE NAME. maxNameLen is v4's own constant at v4's own value, kept in step with
 777	// app/js/config.js's NAME_MAX_LEN (24) by hand, because Gno has no way to read that file at
 778	// build time -- the same hand-maintained pairing v4 documented and v6 inherits. The app's
 779	// NAME_RULE.MAX_LEN must equal this number; TestMaxNameLenMatchesTheAppsRule pins the value
 780	// so a change here fails a test rather than silently diverging from the one rule the app
 781	// shows a player before they pay gas.
 782	// ============================================================================
 783	maxNameLen = 24
 784
 785	// pinOffsetSeconds / pinOffsetRounds: how far into drand's future Claim pins a round, sized
 786	// AS SECONDS, from drand evmnet's own period (beacon.PeriodSeconds, re-exported from this
 787	// project's own bls254 package, not restated here), per MINT.md section 10.5's explicit
 788	// instruction -- never as a round count checked only against a block count, which is the
 789	// exact dimensional trap this project's own history (the SPLASH raffle shipping with a 100x
 790	// wrong round/block ratio, MEMORY.md) says cost real time once already. 60 seconds is twenty
 791	// drand rounds at the 3-second period CurrentRound() at Claim time is comfortably able to
 792	// pin as "not yet signed by anyone, including drand itself" -- see Claim's own comment for
 793	// why the margin matters even though CurrentRound() is pure arithmetic over block time, not
 794	// a relay-dependent estimate.
 795	pinOffsetSeconds = int64(60)
 796)
 797
 798// pinOffsetRounds is a const expression over an imported const (beacon.PeriodSeconds), which Gno
 799// permits across package boundaries exactly as it would within one; TestPinOffsetIsTwentyRounds
 800// pins the arithmetic rather than trusting the comment above.
 801const pinOffsetRounds = pinOffsetSeconds / beacon.PeriodSeconds
 802
 803// kindEnd-style ratchet machinery: deliberately absent. See the package doc.
 804
 805// hatOptions / coatOptions: v4's validHats/validCoats catalogue, as an ORDERED array rather than
 806// a set, because Settle needs to draw a POSITION, not merely validate one. Order is frozen the
 807// moment this realm deploys, exactly as v4's own numEyes/numNoses ordering is frozen -- a part
 808// may be redrawn; it may never be reordered, removed or renumbered (v4's own comment on this).
 809var hatOptions = [4]string{"red", "purple", "green", "navy"}
 810var coatOptions = [4]string{"blue", "teal", "orange", "brown"}
 811
 812// eyesWeights: ECONOMY.md section 3, verbatim -- "eyes across 4 colours at 15%, 3 at 9%, 2 at
 813// 5%, 1 at 3%" -- 4+3+2+1 = 10 entries = numEyes, and 4*15+3*9+2*5+1*3 = 100.
 814// TestWeightTablesSumToOneHundred pins the sum rather than trusting the arithmetic in this
 815// comment; drawWeighted's own fallback additionally makes a drift here fail safe, not silently.
 816var eyesWeights = [10]int64{15, 15, 15, 15, 9, 9, 9, 5, 5, 3}
 817
 818// noseWeights: ECONOMY.md section 3 says only "nose similar across 8 shapes" and gives no exact
 819// percentages -- BATCH-ONE.md section 4a names this directly as real work nobody has done yet.
 820// This is a SHAPE-FAITHFUL INTERPRETATION, not a transcription: four tiers of two shapes each,
 821// 20/20/15/15/10/10/5/5, the same big-gap-then-smaller-gaps shape eyesWeights has, scaled to 8
 822// entries and summing to 100. Flagged here, not quietly presented as sourced, so the owner or a
 823// future author can replace it with real numbers the moment ECONOMY.md section 3 gets them.
 824var noseWeights = [8]int64{20, 20, 15, 15, 10, 10, 5, 5}
 825
 826// ============================================================================
 827// v7 CHANGES 2, 3, 4, 5 AND 6: THE TRAIT TABLES. TRAITS.md sections 1, 2.2, 3.2, 3.3, 4.2, 5.1.
 828// Ids on chain, NAMES OFF CHAIN -- the precedent v6 already set for parts (EyesCount(4) returns
 829// a number; nothing on chain says what eye type 4 looks like), applied to all of this. The
 830// thirty-two public names, the seventeen secret names, the eight signs and the two label-word
 831// columns all stay in TRAITS.md where FUNNY.md can still revise them weekly. A realm cannot be
 832// edited, and revisable wording is the one thing that must never go on one.
 833// ============================================================================
 834
 835const (
 836	// numAxes: the FIVE rolled axes, in the order the dominance permutation indexes them --
 837	// 0 temperament, 1 obsession, 2 bond, 3 fear, 4 boast. Born-under is NOT one of them
 838	// (TRAITS.md section 1.6: "not in the trait vector, not in the dominance order, not in the
 839	// label rule"), which is why bornUnder has no slot in `dominance` and can therefore never be
 840	// loud, never be latent, and never appear in the label -- by construction, not by discipline.
 841	numAxes = 5
 842
 843	// axisTotalWeight: all six weight tables below sum to exactly this. TRAITS.md section 1's
 844	// whole point and the direct answer to BATCH-ONE.md:120 -- a 64-wide cumulative table has NO
 845	// REMAINDER TO GET WRONG, and 64 divides 2^64 so the draw carries exactly zero modulo bias.
 846	// TestEveryAxisTableSumsToSixtyFour pins all six, the way v6's own
 847	// TestWeightTablesSumToOneHundred pins its two.
 848	axisTotalWeight = int64(64)
 849
 850	// numSecrets: TRAITS.md section 3.2's pool, after the owner's 2026-10-17 cut to seventeen.
 851	// SEVENTEEN IS NOT A TUNING PARAMETER; IT IS A PRECONDITION of the first-seventeen probe in
 852	// secretFor -- 17 is PRIME, so every stride in 1..16 is coprime to it and the probe walk
 853	// visits all seventeen ids exactly once. An eighteen-secret pool would let a stride share a
 854	// factor with the modulus, so the walk could cycle without reaching a free id, turning an
 855	// unreachable panic into a reachable one that aborts a claimant's one and only roll.
 856	// TestSecretPoolSizeIsPrime says exactly that.
 857	numSecrets = 17
 858
 859	// firstDistinctCount: TRAITS.md section 3.2.2, agreed by the owner 2026-10-17. "The first
 860	// seventeen mints draw seventeen distinct secrets... From token 18 onward the draw is the
 861	// full seventeen with duplicates free." It is load-bearing and not a nicety: on a pure draw,
 862	// P(at least one secret missing from a batch of 60) = 36.9%, so more than one batch in three
 863	// could not spell the acrostic of section 3.2.1 -- the thing nobody is allowed to break.
 864	firstDistinctCount = int64(17)
 865
 866	// careThresholdFloor / careThresholdSpan: TRAITS.md section 5.1's roll, threshold = 14 + u
 867	// with u a uniform 0-31 draw, range 14-45 inclusive and flat. v7 STORES u, NOT THE
 868	// THRESHOLD: they are the same five bits either way, and storing the draw keeps the +14 floor
 869	// in one named place instead of baked into sixty stored numbers, so a future batch's realm can
 870	// use a different floor without Batch One's stored values meaning something different.
 871	// 32 divides 256 and 2^64, so there is no modulo bias.
 872	careThresholdFloor = int64(14)
 873	careThresholdSpan  = int64(32)
 874
 875	// The four Top Trumps stats, TRAITS.md section 4.2's budget: 22 points across four stats,
 876	// each 1-10, with at least one >= 8 and at least one <= 3. 670 four-tuples sum to 22; 546 of
 877	// them satisfy both constraints. See legalStatLines and init().
 878	numStats        = 4
 879	statPointTotal  = int64(22)
 880	statMin         = int64(1)
 881	statMax         = int64(10)
 882	statHighAtLeast = int64(8)
 883	statLowAtMost   = int64(3)
 884
 885	// numBornUnder: TRAITS.md section 1.6's eight signs. Rolled, not derived -- section 1.6's own
 886	// recommendation and the only option open to this realm, because a derived sign is a function
 887	// of the adoption week and sixty gnomes settling inside one window would all derive the SAME
 888	// sign, which is not a clan system. The question stays live for Batch Two, where minting may
 889	// be spread across a year; deferring costs nothing, because a future batch's realm can derive
 890	// while Batch One's stays rolled and nobody's sign changes.
 891	numBornUnder = 8
 892)
 893
 894// The six 64-wide weight tables, transcribed from TRAITS.md sections 1.1-1.6, each summing to
 895// exactly axisTotalWeight. Entry order is FROZEN the moment this realm deploys, exactly as
 896// numEyes/numNoses ordering already is -- an entry may be reweighted for a future batch; it may
 897// never be reordered, removed or renumbered, because the stored id is what pins each gnome to
 898// what he actually rolled.
 899var temperamentWeights = [6]int64{13, 11, 12, 10, 10, 8}  // section 1.1: Blithe, Grudgeful, Fretful, Insufferable, Theatrical, Stony
 900var obsessionWeights = [8]int64{12, 10, 9, 9, 7, 6, 6, 5} // section 1.2: Cheese, Fence, Book, Hat, Spade, Lantern, Carrot Row, Moss
 901var bondWeights = [6]int64{13, 12, 11, 10, 10, 8}         // section 1.3: Pibbs, Stone, Fennick, Middle Plot, Gran, Tin
 902var fearWeights = [6]int64{14, 12, 11, 10, 10, 7}         // section 1.4: Beetles, Being Forgotten, The Dark, Being Laughed At, Slugs, Shine
 903var boastWeights = [6]int64{12, 11, 11, 11, 10, 9}        // section 1.5: Badger, Oak, Wheel, Frost, Row, Mend
 904var bornUnderWeights = [8]int64{12, 10, 9, 9, 8, 6, 5, 5} // section 1.6: Carrot, Bramble, Acorn, Nettle, Clover, Toadflax, Ember, Thistle
 905
 906// secretTargetKind / secretTargetID: TRAITS.md section 3.2's "Undercuts" column, AS IDS ONLY --
 907// the name and the line itself live off chain, where the register is still being revised
 908// (FUNNY.md). kind 0 means the secret undercuts a BOAST, 1 means a FEAR; the id is the axis id
 909// within that axis. Paired by index: secret i undercuts (secretTargetKind[i], secretTargetID[i]).
 910//
 911// TestSecretTargetsMatchTheAllocation pins the three facts section 3.2 states about this table
 912// and that LINEAGE.md section 3.3's arithmetic depends on: twelve secrets target a boast and
 913// five target a fear; EVERY ONE of the six boasts is the target of exactly two; and the fears
 914// are Beetles 2, Being Forgotten 1, Being Laughed At 1, Slugs 1, The Dark 0 and Shine 0. A
 915// transcription slip in either array would move LINEAGE.md section 3.3's per-edge 0.0489 and
 916// TRAITS.md section 3.3's population 61.7% without changing anything observable, which is
 917// exactly the kind of error that needs a test rather than an eye --
 918// TestSecretWeightingHitsSixtyOnePercent is the second, behavioural lock on the same door.
 919//
 920// A DECLARED CONSEQUENCE OF PUTTING THIS TABLE ON CHAIN, recorded here so the first person to
 921// notice finds the reasoning rather than reporting it: the two arrays publish, in this realm's
 922// own public source, which boast or fear each secret id undercuts, so an off-chain observer
 923// combining Secret/Boast/Fear can determine that a given gnome's secret targets his own boast.
 924// That is not a name, not a definition and not a rarity count, so it is inside TRAITS.md section
 925// 0 rule 2 -- and v6 already settles the precedent in the same direction, because eyesWeights is
 926// both "internal" per ECONOMY.md section 3 and published verbatim in v5's deployed source.
 927// "Internal" means NOT PRINTED ON A CARD, not absent from the source.
 928var secretTargetKind = [numSecrets]int64{1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1}
 929var secretTargetID = [numSecrets]int64{0, 0, 1, 2, 0, 5, 3, 1, 4, 2, 5, 3, 3, 1, 0, 4, 4}
 930
 931const (
 932	// secretTargetKindBoast / secretTargetKindFear: the two values secretTargetKind holds, named
 933	// rather than written as 0 and 1 at the comparison site in secretWeightsFor.
 934	secretTargetKindBoast = int64(0)
 935	secretTargetKindFear  = int64(1)
 936
 937	// secretMatchMultiplier: TRAITS.md section 3.3, "base weight 1 for all seventeen, multiply by
 938	// 8 if the secret's target matches the gnome's rolled boast or rolled fear". One multiplier,
 939	// one comparison, no second table. The dial was recomputed at seventeen secrets and x8 stayed
 940	// the recommendation: it puts 61.7% of gnomes on a secret that undercuts something they say
 941	// out loud, where x4 gives 44.9% and x16 gives 76.0%. The reason not to go higher is that a
 942	// system which ALWAYS punctures the boast is a system whose output you can predict from the
 943	// card.
 944	secretMatchMultiplier = int64(8)
 945
 946	// secretBaseWeight: the other half of the same rule.
 947	secretBaseWeight = int64(1)
 948)
 949
 950// legalStatLines: TRAITS.md section 4.2's 546 legal stat lines, ENUMERATED AT init() FROM THE
 951// RULE rather than transcribed as a literal table, and packed four stats to an int64. See
 952// buildLegalStatLines and init().
 953//
 954// WHY ENUMERATED AND NOT A 546-ROW LITERAL -- auditability, not bytes: a transcription error in a
 955// 546-row table is undetectable by eye, and the rule it transcribes is four comparisons.
 956// TestLegalStatLinesIsExactly546 pins the count, TestEveryLegalStatLineSumsToTwentyTwoAndIsSpiky
 957// pins the property of every row, and TestLegalStatLineOrderIsStable pins that the enumeration
 958// order is the one this realm deployed with -- because the draw indexes into it, so the order is
 959// frozen the moment v7 deploys, exactly as numEyes/numNoses ordering already is.
 960//
 961// WHY AT init() AND NOT INSIDE settleCore: 10,000 iterations once, at deploy, instead of on every
 962// claimant's one transaction. The named risk, recorded rather than hidden: v5's own deploy used
 963// 131.9M gas of a 160M --gas-wanted, which is not a comfortable margin, so MEASURE THIS BEFORE
 964// DEPLOYING (DESIGN.md section 3.6). If it does not fit, the cheap fallback is to build the table
 965// lazily on the first Settle, paid once by whoever settles first; that is not the default because
 966// it makes one arbitrary claimant pay for everybody and puts a conditional in settleCore.
 967var legalStatLines []int64
 968
 969// namedEditionOrder: BATCH-ONE.md section 7's eight names, in the fixed order Settle's
 970// namedPick indexes into. The order carries no meaning -- it is not a ranking -- it only has to
 971// be fixed, which it is, forever, the moment this realm deploys.
 972//
 973// THE FIRST THREE ARE v5's AND v6's, IN v5's ORDER, AND THE FIVE ADDED BY v7 CHANGE 10 ARE
 974// APPENDED RATHER THAN INTERLEAVED. That is deliberate and it is the only thing about this
 975// ordering that is a decision: appending leaves hob/thistle/wick at indexes 0/1/2, so every
 976// worked example in this file's comments, in DESIGN.md and in the test suite that says "pick 0 is
 977// Hob" is still true, and the diff against v6 stays readable. Nothing reads this array by index
 978// except settleCore's own `namedEditionOrder[namedPick]`, which does not care.
 979var namedEditionOrder = [numNamedEditions]string{
 980	"hob", "thistle", "wick",
 981	"tally", "glim", "vigil", "spruce", "yonder",
 982}
 983
 984// namedEditionDisplayNames: BATCH-ONE.md section 7's eight names AS THEY ARE WRITTEN -- "Hob,
 985// Thistle, Wick, Tally, Glim, Vigil, Spruce, Yonder", the form this file's own documentation uses
 986// throughout -- paired by index with namedEditionOrder above.
 987//
 988// WHY A SECOND ARRAY RATHER THAN JUST CAPITALISING namedEditionOrder, which is the change a
 989// reviewer will reach for first: namedEditionOrder's values are not labels, they are KEYS.
 990// settleCore writes characterOwner.Set(character, ...) and tests characterOwner.Has(candidate)
 991// with those exact strings, and the public CharacterOwner(name) reader does a raw tree Get with
 992// no case folding at all -- so CharacterOwner("hob") resolving is a chain fact that an
 993// off-chain caller already depends on, and init() registers the genesis gnome under the matching
 994// lowercase key "sprig". Capitalising namedEditionOrder would move those keys to "Hob" while
 995// leaving "sprig" lowercase, break CharacterOwner("hob") for every existing caller, and it is
 996// unfixable after deploy. The keys stay canonical.
 997//
 998// This array exists ONLY to supply the gnome's `name`, which is a DISPLAY STRING and has always
 999// been a different thing from its canonical key -- see canonicalNameKey's own comment on why the
1000// stored form and the uniqueness key are deliberately two strings. nameOwner is still keyed by
1001// canonicalNameKey(name), which folds "Hob" to "hob", so reservedNameKeys and the uniqueness
1002// index are untouched by this: no new mechanism, just the display side of a separation that was
1003// already here. TestNamedEditionDisplayNamesPairWithTheirCanonicalKeys pins the pairing in both
1004// directions, so an edit to either array that breaks it fails a test rather than shipping a
1005// one-of-one whose display name canonicalises to somebody else's key.
1006var namedEditionDisplayNames = [numNamedEditions]string{
1007	"Hob", "Thistle", "Wick",
1008	"Tally", "Glim", "Vigil", "Spruce", "Yonder",
1009}
1010
1011// namedEditionDisplayName maps a named edition's canonical character key to its display name.
1012// Used by settleCore's mint and mirrored by the test fabricator, so there is exactly one copy of
1013// the mapping. Returns the input unchanged for anything that is not one of the eight editions --
1014// which is unreachable from settleCore (it only ever passes a namedEditionOrder entry) and is a
1015// pass-through rather than a panic because this is a label lookup, not a security boundary.
1016func namedEditionDisplayName(character string) string {
1017	for i, canonical := range namedEditionOrder {
1018		if canonical == character {
1019			return namedEditionDisplayNames[i]
1020		}
1021	}
1022	return character
1023}
1024
1025// genesisGnomeName: DECISIONS.md 2026-10-15 settles the MECHANISM (minted to this realm's own
1026// address, non-transferable, never listable, separate from the batch) and until 2026-10-19 never
1027// settled a NAME anywhere in this project's documents -- "Genesis" was a placeholder that had
1028// been read as a decision for four days.
1029//
1030// SETTLED 2026-10-19: SPRIG. The owner delegated the choice ("ok whatever you think") after being
1031// asked three times. Reasons, so a future reader can disagree with the argument rather than the
1032// taste: "Genesis" is the only word in this realm that comes from blockchains rather than from
1033// Hollowmere, and it is the FIRST gnome anybody meets -- a bad place to break the register. Sprig
1034// is a growing thing, which is what a garden's first gnome should be; it is short enough to sit in
1035// a name slot beside Hob, Wick and Glim without looking like a different kind of word; and it does
1036// not collide with any of the eight reserved names, any born-under sign, or any noun in LORE.md's
1037// fauna or flora. Runner-up was Pip, dropped because it reads as a diminutive of somebody, and
1038// this gnome is nobody's.
1039//
1040// CHANGED FROM v5 IN ONE RESPECT, deliberately: v5's comment ended "it is read by nothing else
1041// in this file", which was true there and is NOT true here. v6 fix 1 writes this string into the
1042// genesis gnome's own `name` field at init() and registers its canonical key in nameOwner, so
1043// changing it also changes which name is taken. It must still canonicalise to one of
1044// reservedNameKeys below, or an ordinary gnome could be named "Sprig" and pose as it --
1045// TestGenesisNameIsReserved pins exactly that, so an edit here that breaks the pairing fails a
1046// test rather than shipping.
1047const genesisGnomeName = "Sprig"
1048
1049// reservedNameKeys: the canonical name keys NO ORDINARY GNOME MAY EVER TAKE -- the eight named
1050// one-of-ones (BATCH-ONE.md section 7's roster, re-read rather than assumed: Hob, Thistle, Wick,
1051// and v7 change 10's Tally, Glim, Vigil, Spruce, Yonder) plus the genesis gnome. Checked by
1052// SetName; see assertNameAssignable.
1053//
1054// THE ARRAY LENGTH IS [numNamedEditions + 1] AND THAT "+ 1" IS THE GENESIS GNOME, so adding a
1055// roster entry without adding its key here is a COMPILE ERROR rather than a silent hole -- which
1056// is the one piece of luck in change 10's four-places-in-step problem and the reason this file
1057// states the length arithmetically instead of writing 9.
1058//
1059// WHY THIS EXISTS SEPARATELY FROM THE nameOwner UNIQUENESS INDEX, which is the subtle part and
1060// the reason a reviewer should not "simplify" one of the two away: the uniqueness index only
1061// protects a name that is ALREADY TAKEN BY A LIVING TOKEN. Hob's cap-one edition may not have
1062// been minted yet -- its namedPick is a pure function of a drand round nobody has rolled -- and
1063// for the whole window before it mints, nameOwner has no "hob" key at all. Without this list,
1064// the FIRST ordinary claimant could name their gnome "Hob", take the key, and the real Hob would
1065// then mint into a collection where its own name already belongs to an impostor. Reservation
1066// covers the pre-mint window; uniqueness covers the post-mint one. Both are needed, and together
1067// they are the "two locks on a door" discipline this file already uses for the genesis gnome.
1068//
1069// Every entry is already in canonical form (lowercase, single-spaced, trimmed), and
1070// TestReservedNameKeysAreCanonical pins that -- a non-canonical entry here would be a key
1071// canonicalNameKey could never produce, i.e. a reservation that silently protects nothing.
1072var reservedNameKeys = [numNamedEditions + 1]string{
1073	"hob", "thistle", "wick",
1074	"tally", "glim", "vigil", "spruce", "yonder",
1075	"sprig",
1076}
1077
1078// ============================================================================
1079// v7 CHANGE 7: THE KINSHIP WEB, STRUCTURE ONLY. LINEAGE.md section 2.4 and section 6's
1080// OVERRULED box.
1081// ============================================================================
1082//
1083// The owner's own words, and the document's own correction: "what rarity and bloodline is good,
1084// stop trying to nerf everything, as long as we don't charge people money, what difference does
1085// it make, we haven't sold anything." -- and: "The corrected position: store the web on chain...
1086// The one argument that survives, and it is about timing, not safety: a realm cannot be edited,
1087// and the 84 relation sentences in section 2 are hours old while the comic register is still
1088// being revised weekly. Store the STRUCTURE -- who is related to whom, which is settled -- and
1089// keep the WORDING off chain where it can still be improved."
1090//
1091// SO: the 84 (from, to, type) rows, as ids. NOT the 84 label sentences, NOT the thirteen lots,
1092// NOT their members, NOT the grandparents' one-liners, NOT the card's line-name field.
1093//
1094// THE LOTS STAY OFF CHAIN DELIBERATELY, and this is the one place the OVERRULED box has to be
1095// read narrowly rather than generously. It names "the 84 relation sentences" as the wording and
1096// "who is related to whom" as the structure; it does not extend to lot membership. And section
1097// 6.5 -- which the box says explicitly SURVIVES and "is the part that was doing the real work
1098// all along" -- forbids LineOf(tokenID) and LineCount(line) BY NAME. A realm that stored the
1099// thirteen lots would be a realm that could answer both, so v7 does not store them. The line a
1100// card prints ("Out of Pell's lot") is derived by the client from LINEAGE.md section 1.1's
1101// published table joined on BatchIndex, which is the same ids-on-chain/names-off-chain split as
1102// everything else here.
1103//
1104// AND THE "SECOND CODE PATH" -- LINEAGE.md section 6.2's third named cost -- DOES NOT EXIST.
1105// This table is deploy-time-constant state. No function writes it. No function in settleCore,
1106// SaveDay, Buy, List, Wake or SetName reads it. The only code that touches it is the two
1107// enumerators and the verification tests. There is nothing to drift, nothing to gate, and
1108// nothing that could accidentally branch on it, because no code path that does anything
1109// consequential can see it.
1110//
1111// WHY THIS IS ALSO LINEAGE.md section 6.4's COMMITMENT, satisfied better than the repo hash it
1112// recommends: section 6.4 wanted sha256 of the section 2.4 block published in the repo, dated,
1113// before the batch opens, and noted the recommendation "got slightly less comfortable at 60"
1114// because "a stranger who mints #47 has no relationship with the repo and no reason to trust an
1115// mtime". THE DEPLOYED REALM IS THE COMMITMENT: dated by a block height, signed by a known key,
1116// immutable, and readable by that stranger without anybody telling him where to look. The
1117// sequence section 6.4 insists on ("take the hash before the mint opens") is satisfied
1118// automatically, because this realm must deploy before AdminAllow can let anybody claim.
1119
1120// The nine kinship type ids, FROZEN ALPHABETICALLY rather than in LINEAGE.md's own frequency
1121// order (cousin 39, neighbour 11, sibling 10, rival 9, not-speaking 5, in-law 4, disputed 3,
1122// latent-kin 2, taken-in 1), because the frequency order is a property of THIS batch and would
1123// change for Batch Two, whereas alphabetical is stable and self-checking. LINEAGE.md section 2.4
1124// declares the vocabulary closed -- "the nine type values are the complete vocabulary; section 5
1125// adds no tenth, and the rebuild at 60 added no tenth either" -- so the ids are safe to freeze.
1126// TestKinTypeNamesAreAlphabeticalAndNine pins both facts.
1127const (
1128	kinCousin      = int64(0)
1129	kinDisputed    = int64(1)
1130	kinInLaw       = int64(2)
1131	kinLatentKin   = int64(3)
1132	kinNeighbour   = int64(4)
1133	kinNotSpeaking = int64(5)
1134	kinRival       = int64(6)
1135	kinSibling     = int64(7)
1136	kinTakenIn     = int64(8)
1137
1138	numKinTypes = 9
1139	numKinEdges = 84
1140)
1141
1142// kinTypeNames: the nine words, paired by index with the ids above. THIS IS A JUDGEMENT CALL
1143// AGAINST THE STRICTEST READING OF "WORDING OFF CHAIN", declared rather than slipped in, and the
1144// argument for it is three-part: the nine words are a CLOSED vocabulary LINEAGE.md declares
1145// complete and that the rebuild at 60 did not extend; they are one word each, carrying none of
1146// the comic register FUNNY.md revises weekly (the thing being revised is the 84 sentences, and
1147// not one of those is here); and without them the table is 84 triples of integers that no client
1148// can render into anything a human reads -- "#14 is a 7 of #6" is not a family.
1149//
1150// IF THE OWNER DISAGREES, THE CHANGE IS ONE FUNCTION. Drop KinTypeName and TotalKinTypes and the
1151// nine words live in LINEAGE.md keyed by id exactly as the thirty-two axis names do. The table
1152// and its enumerators are unaffected and nothing else moves.
1153var kinTypeNames = [numKinTypes]string{
1154	"cousin", "disputed", "in-law", "latent-kin", "neighbour",
1155	"not-speaking", "rival", "sibling", "taken-in",
1156}
1157
1158// kinEdge: one row of LINEAGE.md section 2.4, structure only. `from` is always the LOWER batch
1159// index; every edge is undirected except the three `disputed` rows, where `from` is the gnome
1160// doing the claiming -- and the claimer is the lower index in all three, which is that
1161// document's own constraint on how a dispute may be written rather than a coincidence.
1162type kinEdge struct {
1163	from int64 // 1..60, a batchIndex (NOT a token id -- see batchIndex's own comment)
1164	to   int64 // 1..60, a batchIndex
1165	typ  int64 // 0..8, one of the nine ids above
1166}
1167
1168// kinEdges: LINEAGE.md section 2.4's fenced block, transcribed row by row IN ITS OWN ORDER,
1169// labels omitted. Frozen at deploy; NOTHING IN THIS FILE WRITES TO IT, EVER.
1170//
1171// WRITTEN AS 84 ONE-LINE STRUCT LITERALS WITH NAMED TYPE CONSTANTS, NOT AS PACKED INTEGERS. A
1172// row would squeeze into 16 bits (from 6, to 6, typ 4) and all 84 into eleven int64s, a tenth of
1173// the bytes. It is not worth it: the whole value of this table is that a reviewer can diff it
1174// against LINEAGE.md section 2.4 line by line, and {1, 9, kinSibling} does that where 0x0409
1175// does not. Transcribing 84 rows is the single highest-risk clerical act in building v7, and the
1176// mitigation is that the source looks like the document PLUS TestKinEdgesMatchTheDocument and
1177// TestAllThirtyTwoOriginalEdgesSurvive -- not that the source is small.
1178var kinEdges = [numKinEdges]kinEdge{
1179	{1, 9, kinSibling},
1180	{1, 10, kinNeighbour},
1181	{1, 25, kinCousin},
1182	{2, 19, kinInLaw},
1183	{2, 21, kinRival},
1184	{2, 51, kinNeighbour},
1185	{2, 57, kinRival},
1186	{3, 12, kinDisputed},
1187	{3, 15, kinCousin},
1188	{3, 30, kinCousin},
1189	{4, 7, kinRival},
1190	{4, 11, kinSibling},
1191	{4, 19, kinCousin},
1192	{4, 28, kinCousin},
1193	{5, 12, kinCousin},
1194	{5, 16, kinNeighbour},
1195	{5, 18, kinTakenIn},
1196	{5, 33, kinCousin},
1197	{6, 13, kinRival},
1198	{6, 14, kinSibling},
1199	{6, 31, kinCousin},
1200	{6, 60, kinRival},
1201	{7, 15, kinCousin},
1202	{7, 23, kinLatentKin},
1203	{7, 43, kinNeighbour},
1204	{8, 17, kinCousin},
1205	{8, 22, kinNeighbour},
1206	{8, 24, kinCousin},
1207	{8, 45, kinInLaw},
1208	{9, 14, kinNotSpeaking},
1209	{9, 16, kinCousin},
1210	{9, 50, kinNeighbour},
1211	{10, 21, kinSibling},
1212	{10, 37, kinCousin},
1213	{10, 38, kinNeighbour},
1214	{11, 12, kinDisputed},
1215	{11, 13, kinCousin},
1216	{12, 18, kinCousin},
1217	{13, 20, kinInLaw},
1218	{13, 44, kinDisputed},
1219	{14, 22, kinCousin},
1220	{14, 53, kinNeighbour},
1221	{15, 17, kinNotSpeaking},
1222	{15, 23, kinCousin},
1223	{16, 20, kinCousin},
1224	{16, 25, kinCousin},
1225	{17, 35, kinCousin},
1226	{18, 23, kinNeighbour},
1227	{18, 34, kinCousin},
1228	{19, 27, kinCousin},
1229	{20, 26, kinCousin},
1230	{20, 39, kinNeighbour},
1231	{21, 24, kinRival},
1232	{21, 37, kinCousin},
1233	{22, 32, kinCousin},
1234	{23, 29, kinCousin},
1235	{24, 36, kinCousin},
1236	{24, 48, kinNotSpeaking},
1237	{25, 26, kinSibling},
1238	{27, 28, kinCousin},
1239	{29, 30, kinRival},
1240	{31, 32, kinCousin},
1241	{33, 34, kinNeighbour},
1242	{35, 36, kinRival},
1243	{37, 38, kinCousin},
1244	{39, 40, kinSibling},
1245	{40, 41, kinCousin},
1246	{41, 42, kinCousin},
1247	{42, 43, kinNotSpeaking},
1248	{42, 49, kinNotSpeaking},
1249	{44, 45, kinSibling},
1250	{45, 46, kinCousin},
1251	{46, 47, kinCousin},
1252	{47, 48, kinInLaw},
1253	{49, 50, kinSibling},
1254	{50, 51, kinCousin},
1255	{51, 52, kinRival},
1256	{52, 56, kinLatentKin},
1257	{53, 54, kinSibling},
1258	{54, 55, kinCousin},
1259	{55, 56, kinCousin},
1260	{57, 58, kinSibling},
1261	{58, 59, kinCousin},
1262	{59, 60, kinCousin},
1263}
1264
1265// ============================================================================
1266// Storage
1267// ============================================================================
1268
1269// plot: unchanged from v4. plantedAt == 0 is the empty state; readyAt is stored, not computed,
1270// so a future change to growHours (there is none possible in this file, but the shape is kept
1271// for the day a v6 adds tuning back) could never reach backwards into an already-planted plot.
1272type plot struct {
1273	plantedAt int64
1274	readyAt   int64
1275}
1276
1277// gnome is the whole persisted pet. EVERY FIELD HERE HAS AN EXPORTED READER -- v4's own rule,
1278// carried over verbatim: a realm cannot grow a second chance at this after deploy.
1279//
1280// NOT HERE, DELIBERATELY: a hat/coat the OWNER chose (nothing is chosen at mint any more;
1281// hat/coat are ROLLED exactly as eyes/nose are, and are exactly as permanent as eyes/nose always
1282// were in v4 -- there has never been a function anywhere in this family of realms that changes a
1283// gnome's hat or coat after mint). v5's own comment also listed "a name" here; v6 fix 1 is
1284// precisely the correction of that, and `name` is below.
1285//
1286// character is "" for every ordinary gnome and one of namedEditionOrder's eight strings for a
1287// named one-of-one. eyes/nose/hat/coat are the sentinel -1/-1/""/"" for a named edition: its
1288// look is NOT a point in the ordinary 1,280-look space, it is the art pack's own character art
1289// (Hob's crown and gold coat, Thistle's helmet and axe, Wick's lantern, and v7 change 10's five:
1290// Tally's trowel, Glim's halo, Vigil's slate-and-ink coat, Spruce's top hat and moustache,
1291// Yonder's bandana and parrot), which this contract
1292// does not need to know the pixels of -- only that Character(tokenID) names it, so the app can
1293// render the right thing. A named edition's parts are therefore never counted in
1294// eyesCounts/noseCounts/hatCounts/coatCounts: it is already as rare as a thing can be, at cap
1295// one, counted instead through characterOwner.
1296type gnome struct {
1297	hat       string
1298	coat      string
1299	eyes      int64
1300	nose      int64
1301	character string
1302
1303	// name: v6 FIX 1. The gnome's own name, as its owner typed it, byte-for-byte -- this realm
1304	// NEVER rewrites it (see canonicalNameKey's own comment on why the stored form and the
1305	// uniqueness key are deliberately two different strings).
1306	//
1307	// "" means UNNAMED, which is the state EVERY ordinary gnome is minted in and stays in until
1308	// its owner calls SetName. That is not an oversight to be fixed by defaulting it to
1309	// something: a gnome with no name yet is honestly nameless, and inventing "Gnome #7" on
1310	// chain would make a fabricated string indistinguishable from a chosen one forever.
1311	//
1312	// For the THREE NAMED ONE-OF-ONES and for the GENESIS GNOME this is set ONCE, at mint, to
1313	// their own character name, and SetName refuses them permanently (their identity is the
1314	// whole point of them -- see SetName's own guard). So `name != ""` does not imply "an owner
1315	// chose this"; `character == ""` is the test for that.
1316	name string
1317
1318	// settledRound: v6 FIX 2. The drand round whose published signature produced this gnome's
1319	// parts -- the single number that makes "provably unrigged" (ECONOMY.md section 1) checkable
1320	// by a holder in one state read instead of a transaction-log search. Written once, in
1321	// settleCore, from the pendingClaim's own pinned round; never written again by anything.
1322	//
1323	// 0 means "not produced by a roll at all", which is true of exactly one token for the whole
1324	// life of this realm: the genesis gnome, minted directly in init() and never rolled for. A
1325	// caller can tell the two apart without ambiguity because round 0 is not a real drand round
1326	// and never will be.
1327	settledRound int64
1328
1329	// ------------------------------------------------------------------------------------
1330	// v7: THE PERSONALITY. Fourteen fields, every one of them written ONCE, in settleCore's
1331	// commit block, from the pure drawRoll's output, and never written again by anything.
1332	// Every one has an exported reader EXCEPT careThreshold -- v4's own "every field has a
1333	// reader" rule, carried by v5 and v6, honoured here with one declared exception (see that
1334	// field's own comment and DESIGN.md section 2.6).
1335	//
1336	// FOR THE GENESIS GNOME EVERY ONE OF THESE IS 0, including batchIndex, which is the same
1337	// "never produced by a roll" sentinel settledRound = 0 already is and is true of exactly
1338	// one token for the life of this realm. batchIndex = 0 is therefore unambiguous: the batch
1339	// is 1-indexed and there is no gnome #0.
1340	//
1341	// STORED AS EIGHTEEN EXPLICIT int64 FIELDS, NOT PACKED INTO ONE uint64, which is what
1342	// TRAITS.md section 7 literally says ("51 bits, in one uint64"). Three reasons, in order
1343	// of weight: (1) fourteen shifts and fourteen masks is fourteen chances at a silent
1344	// off-by-one, and a wrong mask returns a PLAUSIBLE number -- temperament 2 instead of 6 --
1345	// with nothing failing, which is precisely the failure mode BATCH-ONE.md:120 warns about,
1346	// introduced into the one function the warning is about, to save 136 bytes a gnome; (2) a
1347	// bit layout is the most load-bearing thing you can ship, load-bearing in exactly the way
1348	// section 7 itself rejects for stored seeds and stored table indexes -- once temperament
1349	// lives at bits 0-2, it lives there forever and every client, indexer and future realm has
1350	// to agree; (3) v6's whole discipline is explicit named fields with explicit readers.
1351	// Section 7's "in one uint64" was making the argument that THE WHOLE PERSONALITY IS SMALL,
1352	// which is true and is still true: it is 65 bits of information (section 7's 51 plus 8 for
1353	// five explicit dominance slots instead of a Lehmer code, plus 6 for batchIndex) and it
1354	// still fits in one machine word. The ENCODING is eighteen fields.
1355	//
1356	// The five rolled axis ids. TRAITS.md sections 1.1-1.5, each drawn from its own 64-wide
1357	// weight table. Axis order here is axis order everywhere: 0 temperament, 1 obsession,
1358	// 2 bond, 3 fear, 4 boast -- which is what `dominance` below indexes.
1359	temperament int64 // 0-5, TRAITS.md section 1.1
1360	obsession   int64 // 0-7, section 1.2
1361	bond        int64 // 0-5, section 1.3
1362	fear        int64 // 0-5, section 1.4
1363	boast       int64 // 0-5, section 1.5
1364
1365	// dominance: the whole permutation of the five axis indexes, loudest first. TRAITS.md
1366	// section 2.2 stores the WHOLE ORDER and not the state labels, and that is the decision
1367	// this realm is making permanently: WHERE THE LOUD/LATENT BOUNDARY FALLS IS AN OFF-CHAIN
1368	// READING of this number. Two states versus three is therefore a constant in the writing
1369	// layer, fully reversible after deploy in either direction, for free -- BUT ONLY IF ALL
1370	// FIVE SLOTS ARE STORED. A realm that stored just "loudest, second-loudest" (5 x 4 = 20
1371	// values, 5 bits, which is what a reviewer optimising for bytes reaches for) would be
1372	// storing the two-state design itself, and the third state would be UNRECOVERABLE
1373	// FOREVER: there would be no stored fact about the other three axes' order for a future
1374	// stimulus engine to re-split. "Two states now is reversible; a third state now is a spec
1375	// nobody can honour" is only true of a realm that keeps all five slots.
1376	//
1377	// The cost of five explicit slots, stated honestly: they can represent invalid states
1378	// ([0,0,0,0,0]) where a 0-119 Lehmer index could not -- so assertRoll checks the
1379	// permutation before anything is committed. The reason to pay that cost rather than spend
1380	// 7 bits on a factorial-base index is that the index needs A CODEC ON BOTH SIDES OF THE
1381	// WIRE, two codecs that must agree forever over a convention nobody has written down, in
1382	// the one function MINT.md section 9 names as needing its own audit pass above all others.
1383	// Five explicit slots have no codec at all: settleCore stores exactly what it computed,
1384	// Dominance returns exactly what is stored, and there is nothing in between for an
1385	// off-by-one to live in.
1386	dominance [numAxes]int64 // a permutation of 0..4; slot 0 is the loudest
1387
1388	// secret: 0-16, TRAITS.md section 3.2. The id only -- the NAME and the LINE live off chain.
1389	// Drawn by secretFor, which is weighted toward the secrets that undercut this gnome's own
1390	// rolled boast or fear, and constrained to be distinct for the first seventeen settles.
1391	secret int64
1392
1393	// The four Top Trumps stats, TRAITS.md section 4.1. Stored as four explicit values and
1394	// NEVER as the index into legalStatLines -- section 7's own reason, twice over: a stored
1395	// index makes the 546-row order permanent, so the table could never be re-derived or
1396	// re-ordered for Batch Two without re-deriving every Batch One gnome.
1397	//
1398	// NOTHING IN ANY REALM COMPARES TWO GNOMES' STATS. There is no Top Trumps game on chain:
1399	// section 4.5's tie rule is social, and v7 ships no reader that takes two token ids.
1400	pluck    int64 // 1-10
1401	nous     int64 // 1-10
1402	grumble  int64 // 1-10
1403	appetite int64 // 1-10
1404
1405	// careThreshold: the 0-31 DRAW, not the threshold. threshold = careThresholdFloor + this.
1406	//
1407	// THE ONE FIELD IN THIS REALM WITH NO EXPORTED READER, AND THE ONLY WRITE-ONLY FIELD IN
1408	// IT. Not read by Settle after it is written, not by SaveDay, not by any reader, not by
1409	// Render. That is unusual enough that a reviewer will try to delete it as dead code --
1410	// hence this paragraph, and hence TestCareThresholdIsStoredAndInRange, which reads it
1411	// directly from the same package because that is how a write-only field is verified.
1412	//
1413	// WHY NO GETTER. Not because it hides the number: a realm's persisted object graph is
1414	// written into every node's store and nothing in Gno encrypts it, so this field is OBSCURE
1415	// RATHER THAN SECRET and this file does not rest on the optimistic reading. The reasons
1416	// are (1) GAMES.md:38's converse -- "a missing enumerator cannot be added later" has a
1417	// mirror image, A SHIPPED GETTER CAN NEVER BE REMOVED, and a getter is what the first
1418	// third-party UI finds in an hour and builds a progress bar on within a week, where a
1419	// serialized object graph is what nobody will bother with: the asymmetry in EFFORT is the
1420	// whole mechanism and it is real even though it is not a guarantee; and (2) the actual
1421	// protection is elsewhere and v7 has to honour it IN STORAGE rather than in omission --
1422	// TRAITS.md section 5.1 point 2: the promptness bonus is a Mood reading taken at the
1423	// moment of care and NEVER STORED, so nobody can tell whether a given care-day scored 1 or
1424	// 2, so nobody can pin a gnome's score closer than a factor of two. Which makes a HARD
1425	// CONSTRAINT on this realm, stated here because it is easy to violate by being helpful:
1426	//
1427	//	v7 STORES NO CARE SCORE, AND STORES NOTHING THAT DISTINGUISHES A 1-POINT CARE-DAY
1428	//	FROM A 2-POINT ONE. recordCaredDay is carried from v6 unchanged -- it records WHICH
1429	//	UTC DAYS were cared for (caredMask, caredDayCount, lastCaredDay) and nothing about
1430	//	the mood at the time. No promptMask, no promptDayCount, no per-day mood log, no
1431	//	"arrived before the mood turned" counter. Any of them would collapse the
1432	//	factor-of-two interval to zero and hand over the countdown that the no-getter rule
1433	//	is only the SECOND line of defence against.
1434	//
1435	// WHY STORE IT AT ALL, given nothing reads it: TRAITS.md section 7 -- derive it from a
1436	// hash and the salt becomes load-bearing forever, and a gnome's threshold must never
1437	// change under him. Stored, it is pinned to the roll that produced it, verifiable by the
1438	// holder against SettledRound and drand's own public API, and independent of any off-chain
1439	// scorer's configuration. One byte per gnome buys all of that.
1440	careThreshold int64 // 0-31, the draw; the threshold is careThresholdFloor + this
1441
1442	// bornUnder: 0-7, TRAITS.md section 1.6. Rolled, stored and read back by BornUnder exactly
1443	// like any other field. WHAT MAKES IT CARD-ONLY IS THAT IT IS NOT IN `dominance` -- the
1444	// permutation is over the five axes, indexed 0-4, and born-under has no index, so it can
1445	// never be loud, never be latent and never appear in the label, by construction. Whether
1446	// the runtime is handed it is an OFF-CHAIN discipline and nothing this realm can enforce;
1447	// this is the half the realm can guarantee, and it does.
1448	bornUnder int64
1449
1450	// batchIndex: 1-60, this gnome's position in Batch One, assigned as settledCount+1 in
1451	// settleCore. 0 for the genesis gnome and for nothing else.
1452	//
1453	// IT EXISTS BECAUSE LINEAGE.md NUMBERS ITS GNOMES #1 TO #60 AND THOSE ARE NOT THIS REALM'S
1454	// TOKEN IDS. init() spends the first seqid on the genesis gnome, so the first CLAIMED
1455	// gnome is token "0000002" -- confirmed on the live v5 chain, not inferred. Without a
1456	// stored join, every client that wants to render a holder's cousins has to know (a) that a
1457	// genesis gnome exists, (b) that it consumed id 1, and (c) that seqid renders as seven
1458	// zero-padded digits: three pieces of internal realm trivia, on a realm that cannot be
1459	// edited, standing between a holder and his own family. Six bits removes all three.
1460	//
1461	// SETTLE ORDER, NOT CLAIM ORDER, and for the same reason the first-seventeen secret rule
1462	// uses settle order: token ids are assigned in settleCore, so batchIndex and the token id
1463	// advance together and never disagree. A client can cross-check (batchIndex should equal
1464	// the token's numeric id minus one) but is never required to.
1465	//
1466	// LINEAGE.md section 1.3 already licenses the consequence for Hob, Thistle and Wick: it
1467	// nominally places them at #1, #12 and #24 and says in the same breath that "no edge in
1468	// section 2 depends on it -- if the roster assigns the three one-of-ones to different ids,
1469	// swap the ids and the web is unchanged". The web attaches to batch indexes; the named
1470	// editions attach to whichever gnomes drew them. Nothing reconciles the two and
1471	// nothing needs to.
1472	//
1473	// THAT ARGUMENT CARRIES v7 CHANGE 10's FIVE NEW EDITIONS UNCHANGED, which is worth saying
1474	// because LINEAGE.md section 1.3 was written when the roster was three and still says three.
1475	// Its nominal #1/#12/#24 placement was already decorative ("no edge depends on it"), and the
1476	// 84 edges are keyed by batch index 1..60 with no reference to a character at all, so a
1477	// roster of eight needs no edge to move, no index to be reserved, and no edit to LINEAGE.md
1478	// for this file to be correct. The stale count there is a documentation lag, not a
1479	// disagreement with the chain.
1480	batchIndex int64
1481
1482	level     int64
1483	gnomeType string
1484	adoptedAt int64 // unix seconds; the moment Settle minted this gnome, or init() for genesis
1485
1486	belly   int64
1487	bellyAt int64
1488	cheer   int64
1489	cheerAt int64
1490
1491	lastPatAt        int64
1492	lastHappyEventAt int64
1493
1494	totalXp int64
1495
1496	xpGarden     int64
1497	xpTinker     int64
1498	harvestCount int64
1499
1500	dailyCareXp  int64
1501	dailyCareDay int64
1502
1503	plots [numPlots]plot
1504
1505	lastTinkerAt int64
1506
1507	// Cared days. v4's own shape, unchanged: the chain stores FACTS (which UTC days were
1508	// cared for), never the STREAK RULE, which stays in app/js/rules.js's streakFrom. See
1509	// v4's gnome struct comment for the full argument; it applies here without edit.
1510	caredDayCount int64
1511	lastCaredDay  int64
1512	caredMask     uint64
1513
1514	lastActionAt int64
1515
1516	// lastWokenAt: unix seconds, the moment THIS token last became somebody's active gnome --
1517	// set at mint (settleCore), at purchase (Buy), and at every explicit Wake, always to "now" at
1518	// that moment, never anything earlier. SaveDay's backfill floor is max(lastActionAt,
1519	// lastWokenAt, now-48h): a token cannot be credited for any period before it was actually the
1520	// active one, closing the gap ATTACK-REVIEW.md goal 7 found -- v4 never needed this field
1521	// because v4 enforced one gnome per address, so there was no second token to backfill against
1522	// while this one slept. See SaveDay's own comment for the full argument.
1523	lastWokenAt int64
1524
1525	acquiredAt int64
1526	saleCount  int64
1527}
1528
1529// listing: unchanged in shape from v4, now simply one of possibly SEVERAL a single owner may
1530// have standing at once (mint one, hold many) -- listings is still keyed by tokenID, which was
1531// always the right key for this and needed no change.
1532type listing struct {
1533	seller     address
1534	priceUgnot int64
1535	buyer      address
1536	listedAt   int64
1537	expiresAt  int64
1538}
1539
1540// feeConfig: unchanged from v4.
1541type feeConfig struct {
1542	recipient  address
1543	rateBps    int64
1544	floorUgnot int64
1545}
1546
1547// pendingClaim is the record Claim writes and Settle consumes: the permanent, irrevocable fact
1548// that this address spent its one roll, and the future drand round it is waiting on.
1549type pendingClaim struct {
1550	round     int64
1551	claimedAt int64
1552}
1553
1554var (
1555	token  *grc721.Token
1556	ledger *grc721.PrivateLedger
1557
1558	nextID seqid.ID
1559
1560	gnomes = avl.NewTree() // tokenID -> *gnome
1561
1562	// ownerTokens: owner address -> *[]string, every tokenID that address currently holds.
1563	// MINT.md section 2a's "hold many": v4 never needed this because v4 never let an address
1564	// hold more than one. See addOwnerToken/removeOwnerToken.
1565	ownerTokens = avl.NewTree()
1566
1567	// activeToken: owner address -> tokenID string, the ONE gnome that is awake for that
1568	// address right now. Absent means "nothing awake" -- reachable only for an address that has
1569	// never settled a claim or bought a gnome, or that has sold its only awake gnome and not yet
1570	// woken another (Buy deliberately does not auto-promote a replacement; see Buy's comment).
1571	activeToken = avl.NewTree()
1572
1573	// lastWakeDay: owner address -> int64 UTC day number, the once-per-day gate on Wake.
1574	lastWakeDay = avl.NewTree()
1575
1576	// lastCaredDayForOwner: owner address -> int64 UTC day number, the most recent calendar day
1577	// on which ANY ONE of this owner's tokens has already banked a cared day. The package doc is
1578	// explicit and unconditional: "exactly one is ever AWAKE... and only the awake one can ever
1579	// gain... a cared day" (see this file's own doc comment). lastWokenAt (see the gnome struct)
1580	// closes the BACKFILL half of ATTACK-REVIEW.md goal 7 -- a token cannot backfill a day it
1581	// spent asleep. This tree closes the LIVE half: without it, an owner could Wake(B) partway
1582	// through a UTC day already spent genuinely caring for A, then genuinely care for B too,
1583	// using only real-time, non-backfilled timestamps, and bank the SAME calendar day on BOTH --
1584	// no backfill panic ever fires, because neither action is backfilled, each is simply real
1585	// care given to whichever gnome was actually awake at the time. That still doubles the one
1586	// property this bookkeeping exists to measure (lockCaredDays' resale lock, and the
1587	// public cared-days streak), for one wallet, once per day, for as many tokens as the owner is
1588	// willing to switch through -- the exact harm goal 7 names, reached by a second, undiscovered
1589	// mechanism. recordCaredDay only ever runs, and this tree only ever advances, when the day
1590	// being recorded is NEWLY banked for this OWNER, not merely newly banked for this TOKEN.
1591	lastCaredDayForOwner = avl.NewTree()
1592
1593	balances = avl.NewTree() // owner address -> int64 acorns, per PLAYER, exactly as v4
1594
1595	listings = avl.NewTree() // tokenID -> *listing
1596
1597	// allowlist: owner address -> true. Admin-managed (AdminAllow/AdminDisallow), gates Claim
1598	// only -- MINT.md section 11: "gate issuance, never the secondary market". Buying and
1599	// selling need no entry here at all.
1600	allowlist = avl.NewTree()
1601
1602	// hasRolled: owner address -> true, PERMANENT, never removed by anything in this file. "One
1603	// roll per wallet, forever" (MINT.md section 1a/2.3), independent of whether the resulting
1604	// gnome is still held, independent of whether the address is later removed from the
1605	// allowlist. Disallowing an address after it has rolled does not and cannot un-roll it.
1606	hasRolled = avl.NewTree()
1607
1608	// pendingClaims: owner address -> *pendingClaim. Written by Claim, removed by Settle. At
1609	// most one per address, because hasRolled already refuses a second Claim from the same one.
1610	pendingClaims = avl.NewTree()
1611
1612	// eyesCounts / noseCounts / hatCounts / coatCounts: counted rarity (MINT.md section 13).
1613	// Keyed by the part's own id/name, value is a lifetime population count, incremented only by
1614	// an ORDINARY Settle (never by a named one-of-one, which is counted separately below, and
1615	// never decremented: there is no burn, and a sale moves a part without minting one).
1616	eyesCounts = avl.NewTree()
1617	noseCounts = avl.NewTree()
1618	hatCounts  = avl.NewTree()
1619	coatCounts = avl.NewTree()
1620
1621	// nameOwner: v6 FIX 1, the uniqueness index. canonicalNameKey(name) -> tokenID. A key is
1622	// present if and only if exactly one living token currently carries a name that canonicalises
1623	// to it, which is what makes "names are unique across the collection" a chain fact rather
1624	// than an app convention.
1625	//
1626	// IT IS KEYED ON THE CANONICAL FORM, NOT THE TYPED FORM, and that is the whole point: with a
1627	// raw-string key, "Hob", "hob", "HOB" and "Hob " are four different keys, so uniqueness would
1628	// be worth nothing and the one-of-one reservation below could be walked straight around by
1629	// changing one letter's case. See canonicalNameKey.
1630	//
1631	// RENAMING FREES THE OLD KEY, and that is REQUIRED, not a convenience. If SetName only ever
1632	// added keys, a single token could lock up an unbounded number of names by renaming over and
1633	// over -- one gnome, one transaction each, every good name in the collection permanently
1634	// unavailable to everybody else. Freeing on rename bounds what any wallet can hold hostage to
1635	// exactly one name per token it owns, and since no address can ever hold more than batchCap
1636	// tokens, the worst case for the entire realm is 60 names held at once by the 60 tokens that
1637	// exist. See SetName's commit block, which removes before it sets.
1638	nameOwner = avl.NewTree()
1639
1640	// characterOwner: one of namedEditionOrder's eight names -> tokenID, present only once that
1641	// edition has actually been minted. Has(name) is both "has this cap-one edition been taken"
1642	// (Settle's own fallback check) and "does it exist yet" (CharacterOwner's reader) -- the same
1643	// single tree answers both questions, which is why there is no separate counter for it.
1644	characterOwner = avl.NewTree()
1645
1646	// batchIndexToToken: int64 batchIndex (as decimal) -> tokenID string. v7's join between
1647	// LINEAGE.md's #1-#60 numbering and this realm's token ids, in the direction a client cannot
1648	// compute for itself. Written once per settle, in settleCore, beside the gnome's own
1649	// batchIndex field; never removed, never rewritten -- a sale moves a token, it does not move
1650	// a batch position. See TokenByBatchIndex.
1651	batchIndexToToken = avl.NewTree()
1652
1653	// secretsDrawnMask: bit i is set once secret i has been drawn by one of the first
1654	// firstDistinctCount mints. Written only while settledCount < firstDistinctCount, read only
1655	// while batchIndex <= firstDistinctCount, NEVER CLEARED by anything -- not by a sale, not by
1656	// a rename, not by anything.
1657	//
1658	// Never-cleared is load-bearing for the monotonicity property in secretFor's own comment.
1659	//
1660	// THIS MASK IS DERIVABLE BY ANYONE, AND THAT IS ACCEPTED. AUDIT-01 YELLOW-2 corrected an
1661	// earlier version of this comment which claimed the mask was "exposed by nothing". It is
1662	// not. TraitsCsv returns `secret` AND `batchIndex` in the same call and TokensPage
1663	// enumerates every token, so one pass over the collection reconstructs this word EXACTLY:
1664	// { Secret(t) : BatchIndex(t) <= firstDistinctCount }. There is no getter for the mask and
1665	// there is no SecretCount, SecretCensus, HoldersOfSecret or IsFirstSeventeen -- but shipping
1666	// no getter does not make the value private, it only keeps it off the path of least
1667	// resistance.
1668	//
1669	// Removing Secret or BatchIndex from the readers is NOT on the table and would be the wrong
1670	// trade: the acrostic is found by READING THE SECRET NAMES, and BatchIndex is what closes
1671	// the join to LINEAGE.md's #1-#60 numbering. DESIGN.md section 5.3 already makes exactly
1672	// this argument for the kinship table -- "a third party can of course build KinOf on top of
1673	// 84 public rows; the concern was never that the data is derivable" -- and the same answer
1674	// applies here.
1675	//
1676	// SO, STATED PLAINLY BECAUSE IT HAS AN OPERATIONAL CONSEQUENCE THE KINSHIP TABLE DOES NOT:
1677	// this mask is a live input to an UNSETTLED draw (secretFor's `taken`), so it is a KNOWN
1678	// INPUT TO ANYONE CHOOSING WHEN TO SETTLE. Nobody should reason about the draw on the
1679	// assumption that a pending claimant cannot see it; he can, from two public readers, at any
1680	// moment. TRAITS.md section 3.2.2 note 3 -- an owner who can see which secrets are taken can
1681	// deduce other gnomes' secrets by elimination -- is therefore a product rule about the UI
1682	// and the marketing, not a technical guarantee, and never was one: v7's SOURCE is public too
1683	// (vm/qfile returns it verbatim), so anyone who looks finds firstDistinctCount = 17 and this
1684	// field.
1685	secretsDrawnMask uint32
1686
1687	mintedCount int64 // reservations made by Claim, gates batchCap; never decremented
1688
1689	// settledCount: completions made by Settle. INHERITED COMMENT CORRECTED, AUDIT-01 YELLOW-2 --
1690	// it read "informational only, see BatchStatus", which was true in v6 and is FALSE IN V7.
1691	// Here it drives batchIndex (settledCount+1, the kinship join's key) and the first-seventeen
1692	// secret window (batchIndex <= firstDistinctCount). With mintedCount it is one of the two
1693	// most load-bearing numbers in the realm; nothing here is informational.
1694	//
1695	// It briefly drove a third thing -- slotsLeft (batchCap - settledCount), the trigger of the
1696	// named-edition guarantee written 2026-10-17. That guarantee was removed on 2026-10-18 (see
1697	// settleCore and AUDIT-01 RED-1) precisely because this counter is public through
1698	// BatchStatus, which made the trigger watchable off chain.
1699	settledCount int64
1700
1701	genesisTokenID string // set exactly once, in init()
1702
1703	rulesFrozen = true // always true: there are no rules to freeze or unfreeze in v5. See AdminState.
1704
1705	fee feeConfig
1706
1707	transferGate string
1708)
1709
1710func init(cur realm) {
1711	if !address(adminAddress).IsValid() {
1712		panic("garden: adminAddress is not a valid address; fix the constant before deploying")
1713	}
1714
1715	cfg := feeConfig{
1716		recipient:  address(saleFeeRecipient),
1717		rateBps:    saleFeeRateBps,
1718		floorUgnot: saleFeeFloorUgnot,
1719	}
1720	assertFeeConfig(cfg, cur.Address())
1721	fee = cfg
1722
1723	token, ledger = grc721.NewToken(collectionName, collectionSymbol, 0, cur)
1724	ledger.RegisterExtension(&saleGateExtension{})
1725
1726	// v7 change 4: TRAITS.md section 4.2's legal stat set, enumerated once from the rule. See
1727	// legalStatLines and buildLegalStatLines. Done here rather than in settleCore so no
1728	// claimant pays for it; done from the rule rather than from a 546-row literal so a
1729	// transcription error is not possible.
1730	legalStatLines = buildLegalStatLines()
1731	if len(legalStatLines) != numLegalStatLines {
1732		// Unreachable: the enumeration is deterministic and TestLegalStatLinesIsExactly546 pins
1733		// the count. Named so init() is correct on its own terms -- the same discipline v4's Buy
1734		// uses for its own unreachable checks, and the stakes here are a DEPLOY that silently
1735		// ships a different draw space from the one the tests measured.
1736		panic("garden: DEPLOY GATE: the legal stat line enumeration did not produce " +
1737			strconv.Itoa(numLegalStatLines) + " rows; TRAITS.md section 4.2's rule and this code disagree")
1738	}
1739
1740	// AUDIT-02 YELLOW-D: THE ONE INVARIANT THE NAMED-EDITION ROSTER CANNOT SURVIVE LOSING, now a
1741	// gate rather than only prose, an array length and two tests. settleCore draws
1742	// `namedPick = seedFor(...) % namedPickRange` and takes namedEditionOrder[namedPick] only when
1743	// namedPick < numNamedEditions -- so if the roster ever grows past the modulus, EVERY EDITION AT
1744	// AN INDEX >= namedPickRange IS PERMANENTLY UNMINTABLE. See numNamedEditions and namedPickRange,
1745	// which both say this in words; this is the same statement in a form a DEPLOY cannot ignore.
1746	//
1747	// WHY AN OFF-BY-ONE HERE IS WORSE THAN A WRONG ROW COUNT, which is the reason it earns a gate of
1748	// its own next to the enumeration check above: a miscounted stat enumeration ships a draw space
1749	// that differs from the measured one -- bad, and the stated reason that gate exists -- but it is
1750	// a DIFFERENT distribution, which means it is visibly wrong in the traits the first settles
1751	// produce. AN ELEVENTH EDITION IS NOT WRONG, IT IS SILENT. Claim succeeds, Settle succeeds,
1752	// assertRoll passes, BatchStatus reports a namedCap that counts it, CharacterOwner answers
1753	// not-found exactly as it does for an edition nobody has drawn yet, and the roster reads complete
1754	// in every reader. The only symptom is a one-of-one that never appears -- indistinguishable from
1755	// the 1-in-550 ordinary bad luck this realm deliberately accepts -- and it is noticed when the
1756	// batch closes, by which time this realm is immutable and a piece of commissioned art has nowhere
1757	// to go. A wrong row count is caught by looking at a gnome; this is caught by nothing.
1758	if numNamedEditions > namedPickRange {
1759		panic("garden: DEPLOY GATE: numNamedEditions (" + strconv.Itoa(numNamedEditions) +
1760			") exceeds namedPickRange (" + strconv.Itoa(namedPickRange) +
1761			"); the named-edition pick is seed % namedPickRange, so every edition at an index >= namedPickRange " +
1762			"can never be drawn and would be permanently unmintable with no error anywhere -- raise " +
1763			"namedPickRange (and re-derive the odds at its declaration) or shrink the roster before deploying")
1764	}
1765
1766	// THE GENESIS GNOME. Minted here, directly, to this realm's own address -- never through
1767	// Claim, never counted against batchCap, never allowlist-gated, because it is not a roll and
1768	// not part of the batch (DECISIONS.md 2026-10-15). No wallet holds this realm's private key,
1769	// so no transaction can ever be signed as its owner: List, Buy and Wake all additionally
1770	// refuse genesisTokenID by name as a second, redundant lock, and saleGateExtension's
1771	// OnTransfer refuses it unconditionally as a third -- the same "two locks on a door that
1772	// already has one" discipline v4's own sale gate uses, because the claim "belongs to
1773	// nobody" is only as strong as the weakest reachable path around it.
1774	id := nextID.Next()
1775	genesisTokenID = id.String()
1776	me := cur.Address()
1777	if err := ledger.Mint(me, grc721.TokenID(genesisTokenID)); err != nil {
1778		panic(err)
1779	}
1780	now := time.Now().Unix()
1781	gnomes.Set(genesisTokenID, &gnome{
1782		character: "sprig",
1783		// v6 FIX 1: the genesis gnome carries its own name from the instant this realm exists.
1784		// It is the one token whose name was never chosen by an owner and never can be -- no
1785		// wallet holds this realm's key, and SetName refuses any token with a character anyway.
1786		name:         genesisGnomeName,
1787		eyes:         -1,
1788		nose:         -1,
1789		level:        1,
1790		adoptedAt:    now,
1791		belly:        meterMax,
1792		bellyAt:      now,
1793		cheer:        meterMax,
1794		cheerAt:      now,
1795		lastActionAt: now,
1796		acquiredAt:   now,
1797		// settledRound stays 0: the genesis gnome was never rolled for. See the field's comment.
1798	})
1799	addOwnerToken(me, genesisTokenID)
1800
1801	// v6 FIX 1: register the genesis name in the uniqueness index, so NameOwner resolves it and
1802	// no ordinary gnome can take it. reservedNameKeys already refuses it independently of this
1803	// write (same two-locks discipline as the genesis transfer refusals above) -- this one makes
1804	// the index COMPLETE, so "every name a living token carries is in nameOwner" holds without an
1805	// exception a future reader would have to know about.
1806	nameOwner.Set(canonicalNameKey(genesisGnomeName), genesisTokenID)
1807
1808	// Registered under its own name, "sprig", in the SAME tree the eight named one-of-ones
1809	// use -- not because it competes with their cap (BatchStatus's namedMinted count iterates
1810	// namedEditionOrder only, which "sprig" is never a member of, so this cannot affect the
1811	// batch), but so CharacterOwner("sprig") resolves instead of silently reporting not-found
1812	// for the one named character that is neither ordinary nor one of the eight. Second-audit-
1813	// pass finding (G7): fixed here, not left as a reader inconsistency.
1814	characterOwner.Set("sprig", genesisTokenID)
1815
1816	// The EVENT type stays "Genesis" after the gnome was named Sprig on 2026-10-19, and that is a
1817	// decision rather than a miss: this names the EVENT -- the genesis of the collection, which
1818	// happens exactly once at deploy -- and not the gnome. The gnome's name travels in the payload,
1819	// so a reader gets both and neither word is doing the other's job.
1820	chain.Emit("Genesis", "tokenId", genesisTokenID, "name", genesisGnomeName)
1821}
1822
1823// assertFeeConfig: unchanged from v4's own gate.
1824func assertFeeConfig(cfg feeConfig, realmAddr address) {
1825	if !cfg.recipient.IsValid() {
1826		panic("garden: DEPLOY GATE: saleFeeRecipient is not a valid address; fix the constant at the top of garden.gno before deploying")
1827	}
1828	if cfg.recipient == realmAddr {
1829		panic("garden: DEPLOY GATE: saleFeeRecipient is this realm's own address, and coins sent there are burned because there is no withdrawal path")
1830	}
1831	if cfg.recipient == address(adminAddress) {
1832		panic("garden: DEPLOY GATE: saleFeeRecipient is the admin/deploy key; the fee recipient must be an address chosen for that purpose, not the key that happens to deploy")
1833	}
1834	if cfg.rateBps < 1 || cfg.rateBps > maxFeeRateBps {
1835		panic("garden: DEPLOY GATE: saleFeeRateBps must be between 1 and " + strconv.FormatInt(maxFeeRateBps, 10) + " basis points; fix the constant at the top of garden.gno before deploying")
1836	}
1837	if cfg.floorUgnot < 1 || cfg.floorUgnot > maxPriceUgnot {
1838		panic("garden: DEPLOY GATE: saleFeeFloorUgnot must be between 1 and " + strconv.FormatInt(maxPriceUgnot, 10) + " ugnot; fix the constant at the top of garden.gno before deploying")
1839	}
1840}
1841
1842// saleGateExtension: v4's shape, plus the genesis refusal. Panics on every transfer except the
1843// one Buy is performing right now, and ALWAYS panics for genesisTokenID regardless of
1844// transferGate -- there is no path, including a future bug in Buy, by which the genesis gnome
1845// should ever be transferable, so this checks it first and unconditionally.
1846type saleGateExtension struct{}
1847
1848func (saleGateExtension) ExtensionKind() string { return "sale-gate" }
1849
1850func (saleGateExtension) OnMint(to address, tid grc721.TokenID) {}
1851
1852func (saleGateExtension) OnTransfer(from, to address, tid grc721.TokenID) {
1853	if tid.String() == genesisTokenID {
1854		panic("garden: the genesis gnome belongs to nobody and can never change hands")
1855	}
1856	if transferGate == "" || transferGate != tid.String() {
1857		panic("garden: a gnome can only change hands through Buy; every other transfer is permanently disabled")
1858	}
1859}
1860
1861func (saleGateExtension) OnBurn(tid grc721.TokenID) {
1862	panic("garden: gnomes cannot be burned")
1863}
1864
1865// ============================================================================
1866// Admin: the allowlist. The only admin-reachable state in this whole realm -- there is no
1867// SetRule, no FreezeRules, no fee steward, no treasury reach, nothing else at all.
1868// ============================================================================
1869
1870// AdminAllow adds addr to the allowlist, so it may call Claim. Adding an address a second time
1871// is a harmless no-op. Adding an address that has already rolled changes nothing -- hasRolled is
1872// checked independently and is permanent regardless of allowlist state.
1873func AdminAllow(cur realm, addr address) {
1874	if !unsafe.OriginSend().IsZero() {
1875		panic("garden: admin calls accept no coins")
1876	}
1877	caller := cur.Previous().Address()
1878	if caller != address(adminAddress) {
1879		panic("garden: only the admin may manage the allowlist")
1880	}
1881	if !addr.IsValid() {
1882		panic("garden: not a valid address")
1883	}
1884	allowlist.Set(addr.String(), true)
1885	chain.Emit("AdminAllow", "admin", caller.String(), "address", addr.String())
1886}
1887
1888// AdminDisallow removes addr from the allowlist. It does NOT, and cannot, undo a roll already
1889// spent: hasRolled is a separate, permanent record, and this function never touches it. Removing
1890// an address that was never allowed, or that has already claimed, is a harmless no-op.
1891func AdminDisallow(cur realm, addr address) {
1892	if !unsafe.OriginSend().IsZero() {
1893		panic("garden: admin calls accept no coins")
1894	}
1895	caller := cur.Previous().Address()
1896	if caller != address(adminAddress) {
1897		panic("garden: only the admin may manage the allowlist")
1898	}
1899	allowlist.Remove(addr.String())
1900	chain.Emit("AdminDisallow", "admin", caller.String(), "address", addr.String())
1901}
1902
1903// ============================================================================
1904// Claim: the one roll. Phase one of two. MINT.md section 10.5 step 1.
1905// ============================================================================
1906
1907// Claim spends the caller's one-and-only roll on this realm and pins a drand round that does
1908// not exist yet. It mints NOTHING -- Settle does that, once the pinned round is actually
1909// published -- because the whole point of this split is that the result must not be computable,
1910// by anyone, including this transaction's own caller, at the moment the roll becomes irrevocable
1911// (MINT.md section 10.2: gno.land has no on-chain entropy at all, so anything decidable inside
1912// one transaction is decidable by the caller before broadcasting it).
1913//
1914// WALLET-ONLY, the same reasoning as v4's Adopt: a gnome minted to a realm's address could never
1915// be recovered, so a batching, sponsor or multicall realm must never be able to spend a roll on
1916// anyone's behalf. The `gnokey maketx run` shape is refused by the identical gate, for the
1917// identical reason (IsUserCall is false for both).
1918func Claim(cur realm) {
1919	if !cur.Previous().IsUserCall() {
1920		panic("garden: claim must be called directly by a wallet, not through another realm; the one roll this spends could never be returned to an address that could not hold the resulting gnome")
1921	}
1922	if !unsafe.OriginSend().IsZero() {
1923		panic("garden: claim accepts no coins; the mint is free, exactly as garden/v4's adopt already refuses coins, and nothing here can return them")
1924	}
1925
1926	caller := cur.Previous().Address()
1927	if !caller.IsValid() {
1928		panic("garden: claim requires a valid caller address")
1929	}
1930
1931	if !allowlist.Has(caller.String()) {
1932		panic("garden: this address is not on the allowlist; ask the owner to add it before claiming")
1933	}
1934
1935	if hasRolled.Has(caller.String()) {
1936		panic("garden: this address has already rolled on this realm; one roll per wallet, forever, whether or not the resulting gnome is still held")
1937	}
1938
1939	if mintedCount >= batchCap {
1940		panic("garden: batch one is closed; all " + strconv.FormatInt(batchCap, 10) + " editions have already been claimed")
1941	}
1942
1943	// CurrentRound() is pure arithmetic over THIS call's own block time (RoundAt(time.Now())),
1944	// not an extrapolation from however recently somebody last relayed -- see MINT.md section
1945	// 10.4's own live measurement of EstimatedLatestRound() erroring under a stale relay while
1946	// CurrentRound() kept working regardless. Pinning pinOffsetRounds past it is therefore safe
1947	// against relay staleness; the margin exists only for ordinary block-time/wall-clock skew.
1948	round := beacon.CurrentRound() + pinOffsetRounds
1949
1950	// ---- commit. Nothing below this line can fail. ----
1951	hasRolled.Set(caller.String(), true)
1952	mintedCount++
1953	pendingClaims.Set(caller.String(), &pendingClaim{round: round, claimedAt: time.Now().Unix()})
1954
1955	chain.Emit("Claim", "claimant", caller.String(), "round", strconv.FormatInt(round, 10))
1956}
1957
1958// ============================================================================
1959// Settle: permissionless. Phase two of two. MINT.md section 10.5 step 2. Two entry points, one
1960// shared core -- see the package doc's "SETTLE CAN NOW PAY FOR ITS OWN BEACON RELAY" section for
1961// the full account of why, what it costs, and the two live-chain facts (beacon.Publish panics
1962// rather than no-ops on an already-published round; beacon.Publish is reachable cross-realm as
1963// beacon.Publish(cross(cur), round, sigHex), already proven by this package's own test harness)
1964// that shape of both functions below depends on.
1965// ============================================================================
1966
1967// pendingClaimFor looks up claimant's pending roll, shared by both Settle and
1968// SettleWithSignature so the "no pending claim" message and lookup logic exist exactly once.
1969func pendingClaimFor(claimant address) *pendingClaim {
1970	pcv := pendingClaims.Get(claimant.String())
1971	pc, ok := pcv.(*pendingClaim)
1972	if !ok || pc == nil {
1973		panic("garden: no pending claim for this address; it must Claim first, or this claim has already been settled")
1974	}
1975	return pc
1976}
1977
1978// Settle computes and permanently mints the result of claimant's pending roll, once the round
1979// Claim pinned has actually been published to the beacon by SOMEBODY -- another claimant's own
1980// SettleWithSignature, a relay, anyone. It does not publish anything itself; call
1981// SettleWithSignature instead if the round is not published yet and the caller has its
1982// signature to offer. ANYONE may call this -- the claimant themself, the project's own relayer, a
1983// stranger -- because by the time it can succeed at all, nothing is left to decide: the drand
1984// signature for the pinned round either exists publicly or it does not, and the parts this mints
1985// are a pure function of (that signature, the claimant's own address), fixed the instant the
1986// round is published, not the instant Settle happens to be called. Simulating this call before
1987// calling it for real previews a fact, not a choice -- exactly the property MINT.md section 10.5
1988// names as the whole fix: "reading the result afterward is a free query, same as any other realm
1989// read."
1990//
1991// Refuses coins for the same reason every other non-paying function here does: there is no
1992// banker to return them, and this is a free, permissionless computation, never a payment.
1993func Settle(cur realm, claimant address) {
1994	if !unsafe.OriginSend().IsZero() {
1995		panic("garden: settle accepts no coins; it is a free, permissionless computation, not a payment")
1996	}
1997	if !claimant.IsValid() {
1998		panic("garden: settle requires a valid claimant address")
1999	}
2000
2001	pc := pendingClaimFor(claimant)
2002
2003	if !beacon.Published(pc.round) {
2004		panic("garden: drand round " + strconv.FormatInt(pc.round, 10) + " has not been relayed to the beacon yet; call Settle again once it has, or call SettleWithSignature with the round's drand signature (fetched from drand's own public API) to relay it yourself in this same transaction -- relaying is permissionless, and so is settling")
2005	}
2006
2007	settleCore(claimant, pc)
2008}
2009
2010// SettleWithSignature is Settle, plus: if claimant's pinned round has not been published yet,
2011// this function publishes it ITSELF first, in this same transaction, using sigHex -- the round's
2012// drand signature, which the caller's own app fetches read-only from drand's public API
2013// (https://api.drand.sh/<chain>/public/<round> or any mirror) and passes straight through
2014// unmodified. This is the function a player's app should call once it has fetched a signature;
2015// Settle (above) remains for a caller with no signature to offer, or that already knows (or does
2016// not care) that somebody else has published the round.
2017//
2018// SAFE EITHER WAY THE ROUND TURNS OUT TO BE, which is the one property this function exists to
2019// guarantee and the live chain fact its shape is built around (see the package doc): beacon/v2's
2020// own Publish PANICS, not no-ops, when the round is already published, so this function checks
2021// beacon.Published first and only calls Publish if that check says no. If the round was published
2022// by someone else (another claimant's own SettleWithSignature, a relay, anyone) between the
2023// moment the caller fetched sigHex and the moment this transaction lands, sigHex is simply never
2024// used -- this function still succeeds, exactly as Settle would have. A bad sigHex, by contrast,
2025// is a real failure: beacon.Publish rejects it with its own "beacon: invalid signature" panic,
2026// which aborts this whole call (and the claim stays pending, to be retried with a correct
2027// signature or left for Settle once someone else relays it).
2028//
2029// Refuses coins for the same reason Settle does and every non-paying function here does: there
2030// is no banker to return them, and this is a free, permissionless computation -- the only payment
2031// anywhere in this call is the ordinary GNOT gas the caller's own transaction spends, to the
2032// network, never to this project (MINT.md section 14).
2033func SettleWithSignature(cur realm, claimant address, sigHex string) {
2034	if !unsafe.OriginSend().IsZero() {
2035		panic("garden: settle accepts no coins; it is a free, permissionless computation, not a payment")
2036	}
2037	if !claimant.IsValid() {
2038		panic("garden: settle requires a valid claimant address")
2039	}
2040
2041	pc := pendingClaimFor(claimant)
2042
2043	if !beacon.Published(pc.round) {
2044		// Permissionless and carries no caller-identity check (verified against the live
2045		// deployed beacon/v2 source -- see the package doc): this realm relaying on a player's
2046		// behalf is exactly as valid as the player relaying directly, or anyone else doing so.
2047		beacon.Publish(cross(cur), pc.round, sigHex)
2048	}
2049
2050	settleCore(claimant, pc)
2051}
2052
2053// settleCore is Settle's and SettleWithSignature's shared tail: the draw, the mint, and every
2054// write that follows, once the caller above has already confirmed the pinned round is published.
2055// Takes no `cur realm` because it makes no cross-realm call of its own -- beacon.Randomness and
2056// beacon.Published below are plain reads, not crossing functions, exactly as they always were
2057// before this change.
2058func settleCore(claimant address, pc *pendingClaim) {
2059	randomnessHex := beacon.Randomness(pc.round)
2060
2061	if mintedCount < settledCount+1 {
2062		// Unreachable: Claim never lets mintedCount fall behind settledCount+1 for a claim that
2063		// is about to settle, because mintedCount is incremented before any pendingClaim this
2064		// function could ever read exists. Named so this function is correct on its own terms,
2065		// the same discipline v4's Buy uses for its own unreachable checks.
2066		panic("garden: batch accounting is inconsistent; refusing to mint")
2067	}
2068
2069	// v7: the second unreachable accounting check, same discipline, for the one piece of state
2070	// the first-seventeen secret rule depends on. popcount(secretsDrawnMask) must equal
2071	// min(settledCount, firstDistinctCount) at the top of every settle: the mask gains exactly
2072	// one bit per settle until the window closes and is never cleared by anything. If it ever
2073	// does not, something has written the mask outside the one place below that may, and the
2074	// acrostic's distinctness guarantee is no longer true of this realm.
2075	expectedMaskBits := settledCount
2076	if expectedMaskBits > firstDistinctCount {
2077		expectedMaskBits = firstDistinctCount
2078	}
2079	if int64(popcount32(secretsDrawnMask)) != expectedMaskBits {
2080		panic("garden: the first-seventeen secret mask disagrees with the settle count; refusing to mint")
2081	}
2082
2083	// batchIndex: SETTLE ORDER, NOT CLAIM ORDER, and getting this backwards would break both the
2084	// kinship join and the distinctness guarantee. mintedCount counts Claims and gates batchCap;
2085	// settledCount counts Settles. Because Settle is permissionless and claims may settle in any
2086	// order at any later time, mintedCount can be 40 while settledCount is 6 -- so keying the
2087	// first-seventeen rule off claim order would key it off a number already spent before the
2088	// relevant gnome exists.
2089	batchIndex := settledCount + 1
2090
2091	// THE NAMED-EDITION PICK. seed % namedPickRange, against the COMPILE-TIME CONSTANT
2092	// namedPickRange, never against a shrinking count of what remains -- see the package doc's
2093	// "flaw found and fixed" section for why that distinction is the whole safety property here.
2094	// namedPick is fixed the instant randomnessHex exists (i.e. the instant the pinned round is
2095	// signed by drand, which nobody controls), and WHEN Settle happens to be called after that
2096	// changes nothing about it.
2097	//
2098	// v7 CHANGE 9, 2026-10-18: this modulus was uint64(batchCap), making each edition a 1-in-60
2099	// shot and leaving a 74.4% chance at least one of the three never existed. It is now its own
2100	// constant at 10, decoupled from the batch size and ten times the rate. The arithmetic, why it
2101	// is not batchCap, the safety argument and the cost are all at namedPickRange's declaration.
2102	//
2103	// v7 CHANGE 10, the same day, DID NOT TOUCH THIS LINE: it raised numNamedEditions from 3 to 8,
2104	// which widens the branch below without changing the draw. This expression is identical to
2105	// v6's apart from change 9's one constant.
2106	namedPick := seedFor(randomnessHex, claimant, "slot") % uint64(namedPickRange)
2107
2108	// The ordinary draw is always computed, unconditionally, whether or not namedPick lands in
2109	// the named range -- so a claimant whose pick collides with an edition someone else already
2110	// settled first has a ready, equally valid fallback rather than needing a second derivation.
2111	eyesSeed := seedFor(randomnessHex, claimant, "eyes")
2112	noseSeed := seedFor(randomnessHex, claimant, "nose")
2113	hatCoatSeed := seedFor(randomnessHex, claimant, "hatcoat")
2114	drawnEyes := int64(drawWeighted(eyesWeights[:], 100, eyesSeed))
2115	drawnNose := int64(drawWeighted(noseWeights[:], 100, noseSeed))
2116	drawnHat := hatOptions[hatCoatSeed%uint64(len(hatOptions))]
2117	drawnCoat := coatOptions[(hatCoatSeed/uint64(len(hatOptions)))%uint64(len(coatOptions))]
2118
2119	// v7 PHASE 1 AND PHASE 2: the fourteen new values, drawn by one pure function and checked
2120	// before anything is committed. r is carried to the commit block unmodified -- nothing below
2121	// may change a trait, and nothing below has access to a draw.
2122	r := drawRoll(randomnessHex, claimant, batchIndex, secretsDrawnMask)
2123	assertRoll(r, secretsDrawnMask)
2124
2125	character := ""
2126	eyes, nose := drawnEyes, drawnNose
2127	hat, coat := drawnHat, drawnCoat
2128	if namedPick < uint64(numNamedEditions) {
2129		candidate := namedEditionOrder[namedPick]
2130		if !characterOwner.Has(candidate) {
2131			// Whichever Settle call lands on chain first among possibly several colliding
2132			// claimants gets this edition; a later one finds characterOwner already holds it and
2133			// falls through to its own ordinary draw instead. This is a transaction-ordering
2134			// race, not a strategy any claimant can win by choosing WHEN to settle their OWN
2135			// claim -- their own pick is already fixed and cannot change -- see the package doc's
2136			// "a real flaw found and fixed" section for why claimedAt-based tie-breaking was
2137			// considered and rejected, and for the precise property this guarantees instead.
2138			character = candidate
2139			eyes, nose = -1, -1
2140			hat, coat = "", ""
2141		}
2142	}
2143
2144	// ------------------------------------------------------------------------------------
2145	// THERE IS NO NAMED-EDITION GUARANTEE, AND THAT IS A DECISION, NOT AN OVERSIGHT.
2146	// Guarantee written 2026-10-17, audited, REMOVED 2026-10-18 by the owner. The named-edition
2147	// path above is v6's, byte-for-byte. AUDIT-01.md RED-1 is the finding this answers.
2148	// ------------------------------------------------------------------------------------
2149	//
2150	// THE ARITHMETIC, KEPT BECAUSE IT IS THE THING BEING ACCEPTED, AND RECOMPUTED FOR v7 CHANGE
2151	// 10's EIGHT EDITIONS RATHER THAN EDITED. The pick above is `seed % namedPickRange`, named if
2152	// the result is under numNamedEditions = 8, and each of the sixty claimants draws
2153	// independently. So for a given edition, P(never drawn by 60 claimants) =
2154	// (9/10)^60 = 0.0017971, and
2155	// P(at least one of the eight never minted) = 1 - (1-0.0017971)^8 = 1.43%.
2156	// A HALF-FILLED BATCH OF 30 IS 1 - (1-(9/10)^30)^8 = 29.3%, which is the number to quote at
2157	// anyone sizing the allowlist. "HOB WAS NEVER BORN" IS STILL A POSSIBLE PERMANENT FACT ABOUT
2158	// THIS REALM, and so now is "TALLY WAS NEVER BORN" -- there is no guarantee here, only a rate.
2159	// Per edition it is a one-in-five-hundred-and-fifty fact; across eight editions the chance that
2160	// SOME ONE OF THEM is missing is 1.43%, UP from 0.54% at three editions, because adding
2161	// editions adds chances to miss one. That increase was accepted knowingly with change 10.
2162	//
2163	// THOSE WERE 1-in-60 AND 74.4% UNTIL 2026-10-18, when the modulus became namedPickRange
2164	// instead of batchCap (v7 change 9). DESIGN.md section 9.1 is where the 74.4% was first
2165	// computed and it describes the odds as they were; v5, which is live, and v6, which is not,
2166	// both still carry 1-in-60. The decision below -- REMOVAL OF THE GUARANTEE -- was taken
2167	// against the 74.4% and is unaffected by the new rate: raising the odds is not a guarantee and
2168	// does not reintroduce one. The owner's position, recorded while that figure stood, was that a
2169	// one-of-one which might never be minted is arguably the better one-of-one; what he asked for
2170	// afterwards was "the rates have to come out during a mint", which is a probability, not a
2171	// promise. Full arithmetic, including why the modulus is its own constant, at namedPickRange.
2172	//
2173	// KNOWN AND ACCEPTED, THEN: not missed. A guarantee WAS written here, in the owner's
2174	// approved form -- "when the number of unsettled slots remaining equals the number of named
2175	// editions still unclaimed, the remaining settles take them" -- as a forced branch at this
2176	// point plus a helper (namedEditionToForce) comparing slotsLeft = batchCap - settledCount
2177	// against the editions characterOwner did not yet hold. It was audited and it was removed.
2178	// NOTHING SHIPPED IN ITS PLACE. The 74.4% is the status quo v5 and v6 already had.
2179	//
2180	// WHY IT WAS REMOVED, which is the part a future reader needs because it inverts the
2181	// argument the removed code made for itself. That code's defence was that it did NOT draw
2182	// against a shrinking modulus -- it assigned DETERMINISTICALLY -- on the reasoning that a
2183	// draw against a shrinking, observable modulus is the gameable shape v6 exists to document.
2184	// THE REASONING RUNS THE OTHER WAY. DETERMINISTIC ASSIGNMENT KEYED ON A SHRINKING PUBLIC
2185	// OBSERVABLE IS STRICTLY WORSE THAN A DRAW AGAINST ONE: with a draw you are still betting,
2186	// and with assignment THE OUTCOME IS CERTAIN THE MOMENT THE PREDICATE HOLDS. The predicate
2187	// was computable off chain by anybody at any moment from two public readers -- BatchStatus
2188	// returns settledCount, CharacterOwner says which editions are gone -- so a claimant did not
2189	// have to guess; he could watch the branch come to him.
2190	//
2191	// AND THE DEFENCE ADDRESSED THE WRONG WITHHOLDING. It argued that Settle is permissionless
2192	// and never expires, so nobody can reserve a late slot: anyone can settle a holdout's claim
2193	// for the price of gas. That is true of a withheld SETTLE and IRRELEVANT TO A WITHHELD
2194	// CLAIM. Settle requires a pendingClaim to exist (see pendingClaimFor); A CLAIM THAT WAS
2195	// NEVER MADE CANNOT BE SETTLED BY ANYBODY. And Claim gates only on hasRolled and
2196	// mintedCount >= batchCap -- it never reads settledCount. So a wallet that simply did not
2197	// claim until 57 slots were settled took a named edition WITH CERTAINTY instead of the
2198	// honest draw (~3/60 = 5% at the 1-in-60 odds that stood when this was found; ~27% of picks
2199	// pointed at an edition after change 9, and ~8/60 = 13% of claimants can actually end up
2200	// holding one after change 10 -- every one of those numbers changes the GAIN and none of them
2201	// changes the CERTAINTY, so the finding is unaffected, and it gets WORSE as the roster grows
2202	// because a wallet holding back for a forced slot would now have had eight to be handed),
2203	// and eight such wallets took the lot. The one-mint-per-wallet cap did
2204	// not contain it, because each of those wallets needs exactly one mint.
2205	//
2206	// IT ALSO MISFIRED WITH NO ATTACKER AT ALL. BATCH-ONE.md section 2's hand-read rolling
2207	// allowlist leaves claim slots free late BY CONSTRUCTION, so the rarest assets in the
2208	// realm would have gone to whoever the admin happened to allowlist last -- an admin-timed
2209	// allocation of the scarcest things here, unremovable after deploy. The rule existed to
2210	// avoid TRAITS.md section 1.7's "the devs gave their friends the good ones"; rewarding the
2211	// latest instead of the quickest hands the owner that lever directly.
2212	//
2213	// WHAT ANY FUTURE ATTEMPT MUST SOLVE BEFORE IT IS WORTH WRITING, so that re-adding this is
2214	// not cheap. It is not enough to pick a cleverer trigger: ANY condition computed from a
2215	// public monotone counter is watchable, and both settledCount and mintedCount are public
2216	// and must stay so. The hard part is THE WITHHELD CLAIM, and NOTHING IN THIS REALM CAN
2217	// COUNTER IT, because AN UNMADE CLAIM IS UNSETTLEABLE -- there is no pendingClaim for a
2218	// third party to settle, no state to sweep, and nobody who can end the holdout's wait for
2219	// him. The move that suggests itself, a claim expiry or settle deadline, IS FORBIDDEN here
2220	// for an unrelated reason: the first-seventeen secret rule's self-healing property depends
2221	// on abandoned claims staying settleable forever (recorded again at that rule, and in
2222	// AUDIT-01 YELLOW-4, which notes the two guarantees were coupled and that the file had not
2223	// noticed). Three alternatives to removal were considered -- a different trigger, moving the
2224	// allocation off chain to a holding address and allocating by a published process, and
2225	// leaving the rule as written -- and removal was chosen. Solve the withheld claim first or
2226	// leave this alone; it is a design problem, not a patch.
2227	settledCount++
2228
2229	id := nextID.Next()
2230	tid := grc721.TokenID(id.String())
2231	if err := ledger.Mint(claimant, tid); err != nil {
2232		panic(err)
2233	}
2234
2235	// v6 FIX 1: a named one-of-one is born with its own name; an ordinary gnome is born NAMELESS
2236	// and stays that way until its owner calls SetName. Nothing is chosen at mint time, including
2237	// a name -- `character` here is a pure function of the drand round, not of anything the
2238	// claimant asked for, so assigning the name that goes with it is not a choice either.
2239	//
2240	// The name written here is the DISPLAY form -- "Hob", not the canonical key "hob" that
2241	// `character` is and that characterOwner is keyed by. See namedEditionDisplayNames for why
2242	// those are two strings and why collapsing them into one would break CharacterOwner.
2243	name := ""
2244	if character != "" {
2245		name = namedEditionDisplayName(character)
2246	}
2247
2248	now := time.Now().Unix()
2249	gnomes.Set(tid.String(), &gnome{
2250		hat:       hat,
2251		coat:      coat,
2252		eyes:      eyes,
2253		nose:      nose,
2254		character: character,
2255		name:      name,
2256		// v6 FIX 2: the drand round that produced this gnome, stored rather than only emitted.
2257		// pc.round is the round Claim pinned and the one beacon.Randomness was just read at, so
2258		// this is the exact number a holder needs to re-derive their own parts from drand's own
2259		// public API and check this realm did not rig them.
2260		settledRound: pc.round,
2261
2262		// v7 PHASE 3: fourteen field writes taken VERBATIM from the roll. Nothing here computes,
2263		// adjusts, clamps or re-derives anything -- if a value needed fixing up at this point,
2264		// the fix would belong in drawRoll where it could be tested without minting, and
2265		// assertRoll above has already refused anything out of range.
2266		temperament:   r.temperament,
2267		obsession:     r.obsession,
2268		bond:          r.bond,
2269		fear:          r.fear,
2270		boast:         r.boast,
2271		dominance:     r.dominance,
2272		secret:        r.secret,
2273		pluck:         r.pluck,
2274		nous:          r.nous,
2275		grumble:       r.grumble,
2276		appetite:      r.appetite,
2277		careThreshold: r.careThresholdDraw,
2278		bornUnder:     r.bornUnder,
2279		batchIndex:    r.batchIndex,
2280
2281		level:        1,
2282		adoptedAt:    now,
2283		belly:        meterMax,
2284		bellyAt:      now,
2285		cheer:        meterMax,
2286		cheerAt:      now,
2287		lastActionAt: now,
2288		lastWokenAt:  now,
2289		acquiredAt:   now,
2290	})
2291
2292	// v7: the kinship join, written beside the gnome's own batchIndex so the two can never
2293	// disagree, and the first-seventeen mask. The mask is written ONLY here, ONLY inside the
2294	// window, and never cleared -- the two facts the monotonicity property in secretFor's
2295	// comment and the popcount check at the top of this function both rest on.
2296	batchIndexToToken.Set(strconv.FormatInt(r.batchIndex, 10), tid.String())
2297	if r.batchIndex <= firstDistinctCount {
2298		secretsDrawnMask |= 1 << uint(r.secret)
2299	}
2300
2301	addOwnerToken(claimant, tid.String())
2302	if character != "" {
2303		characterOwner.Set(character, tid.String())
2304		// v6 FIX 1: a named edition's name is taken the instant it mints. reservedNameKeys
2305		// already refused it to everyone else before this moment (that is the pre-mint half of
2306		// the protection -- see its own comment); this write is the post-mint half, so the
2307		// nameOwner index is complete for every living token without exception.
2308		nameOwner.Set(canonicalNameKey(name), tid.String())
2309	} else {
2310		eyesCounts.Set(strconv.FormatInt(eyes, 10), eyesCount(eyes)+1)
2311		noseCounts.Set(strconv.FormatInt(nose, 10), noseCount(nose)+1)
2312		hatCounts.Set(hat, hatCount(hat)+1)
2313		coatCounts.Set(coat, coatCount(coat)+1)
2314	}
2315
2316	grantStartingPurseIfNew(claimant)
2317
2318	// Auto-wake only if this claimant currently has nothing awake at all (never settled or
2319	// bought a gnome before, or sold their only one). Never otherwise: a claimant who already
2320	// has an awake gnome gets this one asleep in their stash, exactly as a purchase would (Buy's
2321	// own comment explains why switching must always be a deliberate Wake, never a side effect).
2322	if !activeToken.Has(claimant.String()) {
2323		activeToken.Set(claimant.String(), tid.String())
2324	}
2325
2326	pendingClaims.Remove(claimant.String())
2327
2328	chain.Emit(
2329		"Settle",
2330		"claimant", claimant.String(),
2331		"tokenId", tid.String(),
2332		"round", strconv.FormatInt(pc.round, 10),
2333		"character", character,
2334		"eyes", strconv.FormatInt(eyes, 10),
2335		"nose", strconv.FormatInt(nose, 10),
2336		"hat", hat,
2337		"coat", coat,
2338	)
2339}
2340
2341// popcount32 counts the set bits in secretsDrawnMask, for settleCore's accounting check. Written
2342// out rather than imported (math/bits is not used anywhere else in this file) and bounded at 32
2343// iterations, which is free next to a SHA-256.
2344func popcount32(v uint32) int {
2345	n := 0
2346	for i := 0; i < 32; i++ {
2347		if v&(1<<uint(i)) != 0 {
2348			n++
2349		}
2350	}
2351	return n
2352}
2353
2354// ============================================================================
2355// THE SEED DERIVATION. v7 CHANGE 8 -- THE ONE CHANGE ON THIS VERSION'S LIST THAT IS NOT A
2356// FEATURE, AND THE FIRST THING TO READ IN REVIEW. DESIGN.md section 1.2 item 8, section 4.3.
2357// ============================================================================
2358//
2359// v5 and v6 derive every seed with FNV-1a. v7 does not, and the reason is a provable defect
2360// rather than a preference -- the argument in full, because this is a change to a line an
2361// AUDITED realm carries forward and section 0's rule exists to refuse exactly that:
2362//
2363//	FNV-1a, 64-bit, is `h = offset`, then for each input byte `h ^= b; h *= 1099511628211`.
2364//	THE MULTIPLIER IS ODD, so multiplying never touches bit 0, and XOR acts on bit 0
2365//	independently. Therefore bit0(fnv64a(s)) = 1 XOR bit0(s[0]) XOR ... XOR bit0(s[n-1]) --
2366//	bit 0 of the hash is a PARITY of bit 0 of every input byte, not a hash of it. The general
2367//	form: fnv64a(s) mod 2^k depends ONLY on the low k bits of each input byte, because both
2368//	^ and * are closed on the low k bits. FNV-1a has ZERO diffusion into its low bits -- not
2369//	weak diffusion, none. TestFnvBitZeroIsAParity in garden_test.gno proves it against a local
2370//	copy of v6's own implementation, so the reason for this change lives in the suite and not
2371//	only in this comment.
2372//
2373//	WHY v5 AND v6 SURVIVED IT. They take seed % 100, % 100, % 60 and % 4 and nothing else. The
2374//	three large moduli mix high bits in, so the defect reaches them only through their mod-4
2375//	component; the fully-affected pair (hat and coat) is a UNIFORM PLACEHOLDER draw in those
2376//	realms anyway (BATCH-ONE.md section 4a). It is also not exploitable in any of the three
2377//	realms: the outcome also depends on the parity of a drand randomness that does not exist
2378//	yet when Claim commits, so a claimant cannot know which address parity to shop for. It is
2379//	a randomness-QUALITY defect, not a hole, and v5 being immutable with one ordinary gnome on
2380//	it means there is nothing to do about it there.
2381//
2382//	WHY v7 CANNOT SHIP IT. v7 takes seed % 2, % 3, % 5, % 16 and % 32, which v5 and v6 never
2383//	did, and it takes them for the DOMINANCE PERMUTATION -- the field TRAITS.md section 1.7's
2384//	whole duplicate-vector argument rests on (8,776 vectors to 1,053,132 WITH the order; the
2385//	expected fully-identical pairs among sixty from 0.202 to 0.0017). Under FNV-1a the last
2386//	Fisher-Yates swap would not be a coin flip at all: it would be an XOR of three parities,
2387//	so two claimants who pinned the SAME round would have their final swap decided entirely by
2388//	the parity of their own addresses -- the batch splitting into two halves by address parity,
2389//	every member of a half getting the identical swap. At a 3-second drand period with a
2390//	20-round pin, claimants claiming within a minute of each other share a round, which on a
2391//	curated launch day is most of them. That is structure in the batch's dominance orders: one
2392//	of the two most load-bearing numbers in TRAITS.md, quietly wrong, forever, on an immutable
2393//	realm. And "provably unrigged" (ECONOMY.md section 1) is this project's headline claim: a
2394//	holder who re-derives his own gnome and finds that one bit of it is a parity of his own
2395//	address has found something this project should have found first.
2396//
2397// WHAT THIS CHANGE DOES NOT BUY, said plainly because the temptation is to over-claim.
2398// SHA-256 IS NOT THE SOURCE OF UNPREDICTABILITY HERE EITHER -- drand's signature for a
2399// not-yet-existent round is, exactly as MINT.md section 10.5 designs, and v6's own comment on
2400// fnv64a was right about that and is carried forward in spirit. After the pinned round
2401// publishes, every seed is computable by anybody (that is the design: the commit/reveal split
2402// exists so the fact is fixed before anyone can see it); before it publishes nobody can
2403// compute any of them, and the reason is drand's threshold signature alone; and if drand's
2404// threshold is ever broken, v7 is broken, hash or no hash. v7 does not become more
2405// unpredictable. IT BECOMES CORRECTLY DECORRELATED, which is the only job seedFor was ever
2406// doing and the job v6's implementation does not do.
2407//
2408// crypto/sha256 is not a new assumption: arena/v1 uses sha256.Sum256 for its commit hashes and
2409// its own source records cross-checking the output against Node's crypto.createHash('sha256')
2410// on a fixed input; bls254 and beacon/v2 use it too (beacon.Randomness(round) IS
2411// sha256(signature)). fnv64a is DELETED rather than kept beside this -- two hashes in one realm
2412// is an invitation to call the wrong one.
2413
2414// seedDomain: a version tag, so a seed derived by this realm can never collide with one derived
2415// by any other realm or any later version from the same round and claimant. It is part of the
2416// hashed input and therefore frozen the moment v7 deploys.
2417const seedDomain = "gnogotchi/garden/v7/seed/v1"
2418
2419// purposeStrings: EVERY purpose literal v7 passes to seedFor, declared once so that the
2420// properties the independence proof rests on are checkable by a test rather than believed.
2421// DESIGN.md section 4.2. Three things must hold and each has a test:
2422// TestPurposeStringsAreDistinct (a duplicate would silently weld two draws together -- bond and
2423// boast sharing a seed would make a gnome's bond a deterministic function of his boast across
2424// the whole batch, invisible in any single gnome), TestNoPurposeContainsTheSeparator (so the
2425// hashed input is uniquely decodable and two distinct (claimant, purpose) pairs can never build
2426// the same string), and TestEveryPurposeLiteralIsInTheDeclaredList (so a nineteenth draw added
2427// later cannot escape the other tests).
2428//
2429// FROZEN AT DEPLOY, ASYMMETRICALLY: adding a purpose later is impossible (the realm is
2430// immutable) so this list has to be complete now, and CHANGING one is worse than impossible --
2431// it would re-roll every gnome already minted.
2432var purposeStrings = [18]string{
2433	"slot",         // the named-edition pick                      (v6's, unchanged)
2434	"eyes",         // eyes, weighted over 100                      (v6's, unchanged)
2435	"nose",         // nose, weighted over 100                      (v6's, unchanged)
2436	"hatcoat",      // hat and coat, two 2-bit fields               (v6's, unchanged)
2437	"temperament",  // axis 0, weighted over 64
2438	"obsession",    // axis 1, weighted over 64
2439	"bond",         // axis 2, weighted over 64
2440	"fear",         // axis 3, weighted over 64
2441	"boast",        // axis 4, weighted over 64
2442	"bornunder",    // the card-only sign, weighted over 64
2443	"dom0",         // Fisher-Yates swap for slot 4, mod 5
2444	"dom1",         // Fisher-Yates swap for slot 3, mod 4
2445	"dom2",         // Fisher-Yates swap for slot 2, mod 3
2446	"dom3",         // Fisher-Yates swap for slot 1, mod 2
2447	"stats",        // index into the 546 legal stat lines
2448	"secret",       // the weighted first choice over 17
2449	"secretstride", // the first-seventeen probe stride, mod 16
2450	"threshold",    // the care-threshold draw, mod 32
2451}
2452
2453// seedFor derives one independent-looking 64-bit value from the published drand randomness, the
2454// claimant's own address, and a purpose string -- so a single drand round can safely drive
2455// several unrelated draws (which named edition, which eyes, which nose, which hat/coat, and in
2456// v7 fourteen more) for several different simultaneous claimants without any of them correlating
2457// with each other. SIGNATURE, SEPARATOR, ARGUMENT ORDER AND PURPOSE MECHANISM ARE v6's,
2458// UNCHANGED; only the hash moved (see the block above).
2459//
2460// The first eight bytes of the digest, big-endian, assembled by hand rather than through
2461// encoding/binary so the import list grows by exactly one entry and the byte order is visible in
2462// the source instead of in a package somebody has to go and read.
2463func seedFor(randomnessHex string, claimant address, purpose string) uint64 {
2464	sum := sha256.Sum256([]byte(
2465		seedDomain + "|" + randomnessHex + "|" + claimant.String() + "|" + purpose))
2466	return uint64(sum[0])<<56 | uint64(sum[1])<<48 | uint64(sum[2])<<40 |
2467		uint64(sum[3])<<32 | uint64(sum[4])<<24 | uint64(sum[5])<<16 |
2468		uint64(sum[6])<<8 | uint64(sum[7])
2469}
2470
2471// drawIndex is the one unweighted bounded draw in v7: int64(seed % uint64(n)), and that is all.
2472// It exists so that every unweighted bounded draw goes through ONE line of arithmetic that one
2473// person audits once, instead of nine inline % expressions scattered through drawRoll.
2474//
2475// ON MODULO BIAS, because this is the line an auditor should stop on. For a uniform 64-bit seed
2476// and modulus m, the maximum relative deviation of any outcome's probability from 1/m is
2477// strictly less than m / 2^64. v7's moduli: 2, 4, 16, 32 and 64 divide 2^64 and are therefore
2478// EXACTLY unbiased; 3 and 5 are under 2.8e-19; 31/38/45 (the secret's weighted total) under
2479// 2.5e-18; 60 (v6's named pick) under 3.3e-18; 100 (v6's part tables) under 5.4e-18; and 546
2480// (the stat line), the worst case in the realm, under 3.0e-17 -- about seventeen orders of
2481// magnitude below the probability that drand's own threshold signature scheme is broken. It is
2482// not zero and this file does not claim it is.
2483//
2484// THIS IS A DECLARED DISAGREEMENT WITH TRAITS.md section 2.2 and section 4.2, both of which
2485// instruct rejection sampling ("reject a byte >= 256 - 256 mod n", "draw an index 0-545 by
2486// rejection sampling from ten bits"). Those instructions are right about the draws they were
2487// written for and the difference is the WIDTH of the draw, not the use of %: from a single BYTE,
2488// % 5 really does skew one outcome in five by 0.4%, and from TEN BITS against 546 the skew is
2489// 1.87:1 -- rejection is mandatory in both cases. From a 64-bit draw neither hazard exists at
2490// any scale anyone can measure. And rejection is not free in the one function that most needs to
2491// stay simple: it needs a fresh seed per attempt (MORE seed-derivation surface, not less), an
2492// attempt cap (an unbounded loop in Settle is worse than a 1e-17 bias), and a documented
2493// deterministic fallback at the cap -- three new things, each with its own failure mode, to
2494// remove a deviation of one part in 3e16. If a reviewer wants rejection anyway, the change is
2495// confined to this function and nothing else in the design moves.
2496func drawIndex(seed uint64, n int64) int64 {
2497	if n <= 0 {
2498		panic("garden: drawIndex: n must be positive")
2499	}
2500	return int64(seed % uint64(n))
2501}
2502
2503// drawWeighted picks an index into weights (which TestWeightTablesSumToOneHundred pins as
2504// summing to totalWeight) using seed as the source of randomness -- a plain cumulative-sum
2505// bucket walk, MINT.md section 10.5's own recommendation for what Settle should do ("a simple,
2506// bounded index... easy to audit"), applied to a weight table instead of a published array,
2507// which is the same shape of arithmetic.
2508func drawWeighted(weights []int64, totalWeight int64, seed uint64) int {
2509	if totalWeight <= 0 {
2510		panic("garden: drawWeighted: totalWeight must be positive")
2511	}
2512	x := int64(seed % uint64(totalWeight))
2513	var cum int64
2514	for i, w := range weights {
2515		cum += w
2516		if x < cum {
2517			return i
2518		}
2519	}
2520	// Unreachable if weights sums to exactly totalWeight, which the test above pins. Returning
2521	// the last index rather than panicking keeps Settle from ever aborting a claimant's one and
2522	// only roll over an arithmetic slip elsewhere in this file.
2523	return len(weights) - 1
2524}
2525
2526// ============================================================================
2527// v7: THE DRAW. ONE PURE FUNCTION, THEN v6's COMMIT BLOCK.
2528// ============================================================================
2529//
2530// BATCH-ONE.md:120 names Settle as "the one function MINT.md section 9 names as needing its own
2531// audit pass above all others", and the row it appears in warns that drawing several weighted
2532// tables off one random value is "more arithmetic, more surface for an off-by-one or modulo-bias
2533// bug". THAT WARNING WAS WRITTEN AGAINST FOUR DRAWS. v7 has eighteen, plus a permutation, plus a
2534// constrained draw. So the shape below is not chosen for elegance; it is chosen so that eighteen
2535// draws are no harder to audit than four:
2536//
2537//	PHASE 1 -- THE DRAW.    drawRoll: one pure function. No tree read, no tree write, no mint,
2538//	                        no emit, no time.Now(), no cur realm. Everything it needs is an
2539//	                        argument.
2540//	PHASE 2 -- THE CHECKS.  assertRoll: every "unreachable" assertion about the result, in one
2541//	                        block, BEFORE anything is committed.
2542//	PHASE 3 -- THE COMMIT.  v6's own commit block, plus fourteen field writes taken verbatim
2543//	                        from the roll. Nothing below that line can fail.
2544//
2545// drawRoll BEING PURE IS THE SINGLE MOST VALUABLE PROPERTY IN THIS FILE and it buys four things:
2546//
2547//  1. IT IS TESTABLE WITHOUT MINTING ANYTHING. A table-driven test runs it over a large corpus
2548//     of synthetic randomnessHex values and asserts distributions, with no allowlist, no claim,
2549//     no beacon fixture and no token. v6 cannot do this: its draw is welded to ledger.Mint, so
2550//     every distributional test costs a full claim/publish/settle cycle.
2551//  2. IT IS RE-DERIVABLE OFF CHAIN BY A HOLDER, which is what makes v6 fix 2 (SettledRound)
2552//     worth anything. Fetch https://api.drand.sh/<chain>/public/<round>, sha256 the signature to
2553//     get randomnessHex, run the same draws against your own address. If this function is pure,
2554//     "re-derive it yourself" is a complete instruction. If it read realm state, it would not be.
2555//  3. AN AUDITOR READS ONE FUNCTION. "Can this realm rig a gnome" is answered by reading
2556//     drawRoll and checking that settleCore writes the result through unmodified. Nothing in
2557//     phase 3 can change a trait, because phase 3 has no access to a draw -- only to the roll.
2558//  4. THE TWO PLACES THAT GENUINELY NEED REALM STATE ARE VISIBLE AS ARGUMENTS, not buried as
2559//     global reads: batchIndex (settledCount+1) and taken (secretsDrawnMask). THOSE TWO
2560//     PARAMETERS ARE THE COMPLETE LIST OF ORDER-DEPENDENCE IN v7's DRAW, and being able to see
2561//     the whole list in a function signature is the point.
2562
2563// numLegalStatLines: TRAITS.md section 4.2's own count, re-derived from the rule rather than
2564// quoted -- 670 four-tuples in 1-10 sum to 22, and 546 of those have a stat >= 8 and a stat <= 3.
2565const numLegalStatLines = 546
2566
2567// packStatLine / unpackStatLine: four stats, each 1-10, in one int64, one byte each. A packing
2568// here is safe in the way a packing of the GNOME's fields would not be (see the gnome struct's
2569// own comment): this word is a deploy-time table entry that no client ever sees and no stored
2570// gnome ever holds -- settleCore writes the four stats out as four explicit fields.
2571func packStatLine(a, b, c, d int64) int64 {
2572	return a<<24 | b<<16 | c<<8 | d
2573}
2574
2575func unpackStatLine(packed int64) (a, b, c, d int64) {
2576	return (packed >> 24) & 0xff, (packed >> 16) & 0xff, (packed >> 8) & 0xff, packed & 0xff
2577}
2578
2579// buildLegalStatLines enumerates TRAITS.md section 4.2's legal set ONCE, FROM THE RULE: "22
2580// points across four stats, each in 1-10, with at least one stat >= 8 and at least one <= 3".
2581// Four nested loops, four comparisons, 10,000 iterations, at deploy.
2582//
2583// TRAITS.md section 4.2's instruction is explicit and this follows it: "enumerate the legal set
2584// once (it is small and fixed), then draw uniformly from it. Do NOT roll four stats and repair
2585// them -- a repair loop is a modulo-bias bug and a fairness argument waiting to happen."
2586//
2587// THE ENUMERATION ORDER IS FROZEN THE MOMENT THIS REALM DEPLOYS, because drawRoll indexes into
2588// it. TestLegalStatLineOrderIsStable pins it with a checksum over the whole table.
2589func buildLegalStatLines() []int64 {
2590	out := make([]int64, 0, numLegalStatLines)
2591	for a := statMin; a <= statMax; a++ {
2592		for b := statMin; b <= statMax; b++ {
2593			for c := statMin; c <= statMax; c++ {
2594				for d := statMin; d <= statMax; d++ {
2595					if a+b+c+d != statPointTotal {
2596						continue
2597					}
2598					hi, lo := a, a
2599					// Three elements, not numStats: `a` is already seeded into hi and lo
2600					// above. An array sized numStats here would have a fourth, zero-valued
2601					// element, which would make `lo` zero for every row and silently drop the
2602					// "at least one stat <= 3" constraint -- 546 rows would become 586 and
2603					// nothing would fail except a count. Found by the init() deploy gate while
2604					// building this file, recorded rather than quietly fixed.
2605					for _, v := range [3]int64{b, c, d} {
2606						if v > hi {
2607							hi = v
2608						}
2609						if v < lo {
2610							lo = v
2611						}
2612					}
2613					// The two extra constraints are what kill 6/5/6/5 -- "a gnome with no
2614					// strength and no weakness, who is the one card nobody ever plays".
2615					if hi < statHighAtLeast || lo > statLowAtMost {
2616						continue
2617					}
2618					out = append(out, packStatLine(a, b, c, d))
2619				}
2620			}
2621		}
2622	}
2623	return out
2624}
2625
2626// roll is everything one claimant's draw produces: one field per stored value, and nothing else.
2627// It is a plain value with no pointers and no realm state, returned by drawRoll and consumed by
2628// assertRoll and settleCore's commit block.
2629type roll struct {
2630	temperament int64
2631	obsession   int64
2632	bond        int64
2633	fear        int64
2634	boast       int64
2635
2636	dominance [numAxes]int64
2637
2638	secret int64
2639
2640	pluck    int64
2641	nous     int64
2642	grumble  int64
2643	appetite int64
2644
2645	careThresholdDraw int64
2646	bornUnder         int64
2647	batchIndex        int64
2648}
2649
2650// drawRoll is PHASE 1: the whole audit surface for v7's fairness, as a pure function of its four
2651// arguments. The draws are listed in the order they are computed, which is fixed -- NOTHING
2652// DEPENDS ON THE ORDER, because every draw has its own purpose string and therefore its own
2653// seed, so reordering them would change nothing -- but it is fixed anyway, because a fixed order
2654// is what makes "re-derive it yourself" a checkable instruction.
2655//
2656// batchIndex is settledCount+1 and taken is secretsDrawnMask; both are passed in, and they are
2657// the only order-dependent inputs in the whole draw. Everything else is (randomnessHex,
2658// claimant), which is fixed the instant the pinned round is signed by drand.
2659func drawRoll(randomnessHex string, claimant address, batchIndex int64, taken uint32) roll {
2660	var r roll
2661	r.batchIndex = batchIndex
2662
2663	// The five axes and born-under: six weighted draws over six 64-wide tables, each with its
2664	// own seed. 64 divides 2^64, so each of these carries exactly zero modulo bias.
2665	r.temperament = int64(drawWeighted(temperamentWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "temperament")))
2666	r.obsession = int64(drawWeighted(obsessionWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "obsession")))
2667	r.bond = int64(drawWeighted(bondWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "bond")))
2668	r.fear = int64(drawWeighted(fearWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "fear")))
2669	r.boast = int64(drawWeighted(boastWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "boast")))
2670	r.bornUnder = int64(drawWeighted(bornUnderWeights[:], axisTotalWeight, seedFor(randomnessHex, claimant, "bornunder")))
2671
2672	// The dominance permutation: four draws, four distinct seeds, four DESCENDING moduli.
2673	r.dominance = dominanceFor(randomnessHex, claimant)
2674
2675	// The stat line: one uniform index into the 546 legal lines, unpacked into four explicit
2676	// values. The index is never stored (see the gnome struct).
2677	statIdx := drawIndex(seedFor(randomnessHex, claimant, "stats"), int64(len(legalStatLines)))
2678	r.pluck, r.nous, r.grumble, r.appetite = unpackStatLine(legalStatLines[statIdx])
2679
2680	// The secret: weighted toward the ones that undercut this gnome's OWN rolled boast or fear,
2681	// and distinct from every secret already drawn if this is one of the first seventeen settles.
2682	// THIS IS THE ONLY DRAW THAT READS ANY REALM STATE, and it reads it as the `taken` argument.
2683	r.secret = secretFor(randomnessHex, claimant, r.boast, r.fear, batchIndex, taken)
2684
2685	// The care threshold draw: 0-31, flat, 32 divides 2^64. Stored, read by nothing.
2686	r.careThresholdDraw = drawIndex(seedFor(randomnessHex, claimant, "threshold"), careThresholdSpan)
2687
2688	return r
2689}
2690
2691// dominanceFor draws the dominance order: Fisher-Yates over {0,1,2,3,4}, descending -- swap slot
2692// 4 with a slot drawn from 0-4, slot 3 with one from 0-3, slot 2 from 0-2, slot 1 from 0-1. Four
2693// draws, four distinct seeds, four distinct moduli.
2694//
2695// TRAITS.md section 2.2's two prohibitions are both honoured and both are worth restating as the
2696// things NOT to do:
2697//
2698//   - NOT seed % 120. 120 is not a power of two, and more importantly a single index would need
2699//     the Lehmer codec the gnome struct's own comment rejects.
2700//   - NOT seed % 5 FIVE TIMES. That maps 5^5 = 3,125 outcomes onto 120 permutations
2701//     non-uniformly and is the classic naive-shuffle bug. Four descending moduli is the correct
2702//     Fisher-Yates, and TestDominanceMatchesAnIndependentFisherYates compares this against a
2703//     reference implementation written separately, which is the only way to catch somebody
2704//     reintroducing the naive version later.
2705//
2706// ALL 120 ORDERS ARE LEGAL, so there is no post-draw constraint, no rejection and no loop. That
2707// is the realm-side dividend of TRAITS.md section 2.1's two-state recommendation, and it is a
2708// stronger argument for two states than any of the four section 2.1 gives: under THREE states
2709// with FUNNY.md section 6.5's fix applied, only 96 of the 120 orders would be legal (every order
2710// except the twenty-four that put Temperament in the single dormant slot), and an illegal-order
2711// exclusion is a REJECTION LOOP INSIDE THE DRAW -- Fisher-Yates, then a check, then a redraw --
2712// with an unbounded worst case and a fallback nobody wants to specify, in the one function
2713// BATCH-ONE.md:120 is a warning about. Two states make all 120 legal, so the draw has no
2714// constraint, no check and no loop.
2715func dominanceFor(randomnessHex string, claimant address) [numAxes]int64 {
2716	order := [numAxes]int64{0, 1, 2, 3, 4}
2717	// i counts down over the slots still to be fixed; the modulus descends 5, 4, 3, 2.
2718	for i := numAxes - 1; i >= 1; i-- {
2719		j := drawIndex(seedFor(randomnessHex, claimant, "dom"+strconv.Itoa(numAxes-1-i)), int64(i+1))
2720		order[i], order[j] = order[j], order[i]
2721	}
2722	return order
2723}
2724
2725// secretWeightsFor builds one gnome's secret weight table from TRAITS.md section 3.3's rule:
2726// base weight 1 for all seventeen, MULTIPLIED BY 8 if the secret's target matches this gnome's
2727// rolled boast or rolled fear. One multiplier, one comparison, no second table.
2728//
2729// The total it returns is 31 + 7k with k in {0, 1, 2} -- 31, 38 or 45 and NO OTHER VALUE -- which
2730// is the check that the table is right, because every gnome's boast carries exactly two secrets
2731// and k is the number aimed at his rolled fear (2 for Beetles, 1 for Being Forgotten / Being
2732// Laughed At / Slugs, 0 for The Dark and Shine). TestSecretWeightTotalIsAlwaysThirtyOneOrThirtyEightOrFortyFive
2733// pins exactly that over every (boast, fear) pair, exhaustively.
2734//
2735// THIS IS THE ONE PLACE IN v7 WHERE TRAIT DATA IS COMPARED TO ANYTHING, and it is worth being
2736// precise about what the comparison is and is not: it reads a gnome's own just-drawn boast and
2737// fear, and it produces a weight table that produces a secret id. It gates nothing, it changes no
2738// fee, it grants no eligibility, and it never sees another gnome. The invariant stated once:
2739// NO FUNCTION IN v7 BRANCHES ON A TRAIT, A STAT, A LINEAGE EDGE OR A SECRET -- not in a guard,
2740// not in a loop condition, not in a fee calculation, not in an eligibility check -- and this
2741// weighting is the single exception the invariant is written around.
2742func secretWeightsFor(boast, fear int64) ([numSecrets]int64, int64) {
2743	var w [numSecrets]int64
2744	total := int64(0)
2745	for i := 0; i < numSecrets; i++ {
2746		weight := secretBaseWeight
2747		if secretTargetKind[i] == secretTargetKindBoast && secretTargetID[i] == boast {
2748			weight = secretBaseWeight * secretMatchMultiplier
2749		}
2750		if secretTargetKind[i] == secretTargetKindFear && secretTargetID[i] == fear {
2751			weight = secretBaseWeight * secretMatchMultiplier
2752		}
2753		w[i] = weight
2754		total += weight
2755	}
2756	return w, total
2757}
2758
2759// secretFor is the one genuinely new mechanism in v7 and the one to read twice.
2760//
2761// THE RULE BEING IMPLEMENTED (TRAITS.md section 3.2.2, agreed by the owner 2026-10-17): "The
2762// first seventeen mints draw seventeen distinct secrets. For tokens 1-17 the weighted draw of
2763// section 3.3 runs over the not-yet-drawn subset of the pool only. From token 18 onward the draw
2764// is the full seventeen with duplicates free."
2765//
2766// IT IS NOT IMPLEMENTED AS THE LITERAL SUBSET DRAW, and the reason is the flaw v6 exists to
2767// document. A renormalised weighted draw over the free secrets has a modulus of "the sum of the
2768// weights of the free secrets" -- A SHRINKING, OBSERVABLE, TIME-VARYING NUMBER, which is v6's
2769// package doc word for word: "THAT IS PEEKABLE AND GAMEABLE... a claimant whose seed is already
2770// fixed can simply precompute, for every possible remainingTotal, whether that value would win
2771// them [the thing they want] -- and then wait, costing nothing." Worse, it is not monotone:
2772// removing a secret reshuffles which index every seed maps to, so waiting CAN improve the
2773// outcome. It would also need DIFFERENT ARITHMETIC IN THE TWO PATHS -- a filtered cumulative
2774// walk for the first seventeen and an unfiltered one for the rest -- which is BATCH-ONE.md:120's
2775// warning exactly.
2776//
2777// Instead: one weighted walk, called IDENTICALLY by the first gnome and the sixtieth, plus a
2778// probe with a fall-through made of two modulos by compile-time constants. THE PROBE IS NOT A NEW
2779// INVENTION: it is v6's own audited named-edition pattern generalised -- draw against a constant
2780// modulus; if what you drew is taken, fall through -- with the fall-through made a full cycle
2781// instead of one step deep.
2782//
2783// SIX PROPERTIES, EACH WITH ITS PROOF, because this is where the audit will live.
2784//
2785//	P1 -- IT ALWAYS TERMINATES, AND INSIDE THE WINDOW IT ALWAYS RETURNS A FREE SECRET. The loop
2786//	is bounded at numSecrets iterations by construction. Inside the window at most
2787//	batchIndex-1 <= 16 bits of `taken` are set, and by P2 the walk visits all seventeen ids, so
2788//	at least one is free. The panic below is therefore unreachable; it is named rather than
2789//	omitted, v6's own discipline.
2790//
2791//	P2 -- THE WALK VISITS ALL SEVENTEEN. numSecrets = 17 is PRIME, so gcd(stride, 17) = 1 for
2792//	every stride in [1, 16], so {first + i*stride mod 17 : i = 0..16} is all of Z/17.
2793//	TestSecretProbeVisitsAllSeventeen proves it EXHAUSTIVELY over all 17 x 16 = 272 (first,
2794//	stride) pairs, and TestSecretPoolSizeIsPrime pins the precondition.
2795//
2796//	P3 -- THE STRIDE DRAW HAS EXACTLY ZERO MODULO BIAS. 16 divides 2^64.
2797//
2798//	P4 -- THE ONLY REALM STATE CONSULTED IS `taken`, AND ONLY INSIDE THE WINDOW. drawRoll takes
2799//	it as an argument. There is no other global read anywhere in the draw.
2800//
2801//	P5 -- MONOTONICITY: WAITING CAN NEVER IMPROVE A CLAIMANT'S OUTCOME. `taken` only ever gains
2802//	bits. A claimant's probe ORDER is fixed by his own seed the instant the pinned round
2803//	publishes and cannot change. So if T1 is a subset of T2, the first free id in a given walk
2804//	under T1 sits at an index less than or equal to its index under T2. THERE IS NO
2805//	SETTLE-TIMING STRATEGY HERE, which is the exact property v6's package doc identifies as the
2806//	one that matters ("nobody can improve THEIR OWN odds by choosing when to settle") -- and in
2807//	the stronger direction, because the incentive actively points at settling promptly rather
2808//	than merely being indifferent. TestSecretIsMonotoneUnderWaiting is the proof.
2809//
2810//	P6 -- THE RESIDUAL, STATED PRECISELY AND NOT ASPIRATIONALLY. A third party can settle OTHER
2811//	claims before a claimant settles his, pushing him further down his own fixed probe list.
2812//	That costs the griefer gas and buys him nothing, and the ceiling on the harm is TRAITS.md
2813//	section 3.2.2 note 1's own already-accepted cost: "his gnome may hold a secret that
2814//	undercuts nothing he says." All seventeen secrets are one line each and note 2 is explicit
2815//	that "nobody's secret is better than anybody else's" -- so this is a transaction-ordering
2816//	race over an outcome with no prize, the same category v6 already analysed and accepted for
2817//	the named-edition collision, at strictly lower stakes.
2818//
2819// WHAT THE DEVIATION FROM THE LITERAL RULE COSTS, measured rather than waved at: the probe and
2820// the subset draw agree EXACTLY at batchIndex 1 (nothing taken, so both are the plain weighted
2821// draw) and are FORCED IDENTICALLY at batchIndex 17 (a pool of one). In between they differ only
2822// in WHICH free secret a claimant lands on, never in how many free secrets there are. TRAITS.md
2823// section 3.2.2 note 1 has already priced that as "a fractional softening of a soft rule" against
2824// "a guaranteed solvable acrostic".
2825func secretFor(randomnessHex string, claimant address, boast, fear, batchIndex int64, taken uint32) int64 {
2826	// STEP 1. The weighted first choice. IDENTICAL CODE, IDENTICAL TABLE, IDENTICAL MODULUS for
2827	// the first gnome and the sixtieth.
2828	w, total := secretWeightsFor(boast, fear)
2829	first := int64(drawWeighted(w[:], total, seedFor(randomnessHex, claimant, "secret")))
2830
2831	// STEP 2. Past the forced window, that is the answer. Duplicates free.
2832	if batchIndex > firstDistinctCount {
2833		return first
2834	}
2835
2836	// STEP 3. Inside it, walk THIS CLAIMANT'S OWN probe sequence to the first free secret.
2837	stride := 1 + drawIndex(seedFor(randomnessHex, claimant, "secretstride"), int64(numSecrets-1))
2838	id := first
2839	for i := 0; i < numSecrets; i++ {
2840		if taken&(1<<uint(id)) == 0 {
2841			return id
2842		}
2843		id = (id + stride) % numSecrets
2844	}
2845	// Unreachable by P1 above. Named rather than omitted so this function is correct on its own
2846	// terms: if it ever fires, something has written secretsDrawnMask outside the one place that
2847	// may, and aborting is better than minting a gnome with a duplicate secret inside the window
2848	// the acrostic depends on.
2849	panic("garden: the first seventeen secret pool is exhausted; refusing to mint")
2850}
2851
2852// assertRoll is PHASE 2: every range assertion about a roll, in one block, before anything is
2853// committed. EVERY ONE OF THESE IS UNREACHABLE IF drawRoll IS CORRECT -- which is exactly v6's
2854// own discipline for its batch-accounting check ("named so this function is correct on its own
2855// terms"), and it means a drawRoll bug produces a LOUD ABORT rather than a silently malformed
2856// gnome that can never be fixed, on a realm that cannot be edited.
2857//
2858// taken is the SAME secretsDrawnMask that was handed to drawRoll, and it is here for the one
2859// assertion that range-checking cannot express: FREEDOM, not merely range. See the secret check
2860// below for why this one abort has to happen on the offending mint rather than one settle later.
2861func assertRoll(r roll, taken uint32) {
2862	if r.temperament < 0 || r.temperament >= int64(len(temperamentWeights)) {
2863		panic("garden: roll check: temperament out of range; refusing to mint")
2864	}
2865	if r.obsession < 0 || r.obsession >= int64(len(obsessionWeights)) {
2866		panic("garden: roll check: obsession out of range; refusing to mint")
2867	}
2868	if r.bond < 0 || r.bond >= int64(len(bondWeights)) {
2869		panic("garden: roll check: bond out of range; refusing to mint")
2870	}
2871	if r.fear < 0 || r.fear >= int64(len(fearWeights)) {
2872		panic("garden: roll check: fear out of range; refusing to mint")
2873	}
2874	if r.boast < 0 || r.boast >= int64(len(boastWeights)) {
2875		panic("garden: roll check: boast out of range; refusing to mint")
2876	}
2877	if r.bornUnder < 0 || r.bornUnder >= int64(len(bornUnderWeights)) {
2878		panic("garden: roll check: bornUnder out of range; refusing to mint")
2879	}
2880
2881	// The permutation check the five-slot encoding needs and a Lehmer index would not: every
2882	// axis index 0..4 present exactly once.
2883	var seen [numAxes]bool
2884	for _, slot := range r.dominance {
2885		if slot < 0 || slot >= numAxes {
2886			panic("garden: roll check: dominance holds a value that is not an axis index; refusing to mint")
2887		}
2888		if seen[slot] {
2889			panic("garden: roll check: dominance is not a permutation; refusing to mint")
2890		}
2891		seen[slot] = true
2892	}
2893
2894	if r.secret < 0 || r.secret >= numSecrets {
2895		panic("garden: roll check: secret out of range; refusing to mint")
2896	}
2897
2898	// AUDIT-01 YELLOW-3: INSIDE THE WINDOW, THE SECRET MUST ALSO BE FREE. Unreachable if
2899	// secretFor is correct -- its probe returns the first id with a clear bit in `taken`, or
2900	// aborts -- but the range check above cannot see a duplicate, and the consequence of not
2901	// seeing one is not local. settleCore's popcount check fires at the TOP of a settle, so a
2902	// taken id drawn inside the window would mint SUCCESSFULLY (range ok, `mask |= 1 << secret`
2903	// a no-op, settledCount++ lands) and then panic EVERY SUBSEQUENT SETTLE forever: there is no
2904	// admin mint, no migration and no repair path, so one malformed gnome would be a permanent
2905	// denial of the rest of the mint. Checking it here aborts ONLY the offending mint, which is
2906	// this function's own stated discipline -- a loud abort rather than a silently malformed
2907	// gnome. The downstream popcount assertion stays as well; it is cheap, and it is a genuine
2908	// second line against a write to the mask from somewhere this check cannot see.
2909	if r.batchIndex <= firstDistinctCount && taken&(1<<uint(r.secret)) != 0 {
2910		panic("garden: roll check: the drawn secret is already taken inside the first seventeen; refusing to mint")
2911	}
2912
2913	// The stats, against the rule rather than against the table they came from -- so a corrupt
2914	// table entry is caught here too, not merely a corrupt index.
2915	stats := [numStats]int64{r.pluck, r.nous, r.grumble, r.appetite}
2916	sum, hi, lo := int64(0), stats[0], stats[0]
2917	for _, v := range stats {
2918		if v < statMin || v > statMax {
2919			panic("garden: roll check: a stat is outside 1-10; refusing to mint")
2920		}
2921		sum += v
2922		if v > hi {
2923			hi = v
2924		}
2925		if v < lo {
2926			lo = v
2927		}
2928	}
2929	if sum != statPointTotal {
2930		panic("garden: roll check: the four stats do not sum to 22; refusing to mint")
2931	}
2932	if hi < statHighAtLeast || lo > statLowAtMost {
2933		panic("garden: roll check: the stat line is not spiky (needs one >= 8 and one <= 3); refusing to mint")
2934	}
2935
2936	if r.careThresholdDraw < 0 || r.careThresholdDraw >= careThresholdSpan {
2937		panic("garden: roll check: the care threshold draw is outside 0-31; refusing to mint")
2938	}
2939	if r.batchIndex < 1 || r.batchIndex > batchCap {
2940		panic("garden: roll check: batchIndex is outside 1-" + strconv.FormatInt(batchCap, 10) + "; refusing to mint")
2941	}
2942}
2943
2944// ============================================================================
2945// Wake: switching which gnome is awake. MINT.md section 2a.
2946// ============================================================================
2947
2948// Wake makes tokenID the caller's awake gnome. Free, and limited to once per UTC day -- the same
2949// day-boundary discipline SaveDay's own dailyCareDay already uses, chosen over a GNOT fee for the
2950// reason MINT.md section 2a gives: "a cost that cannot be bought, only lived".
2951func Wake(cur realm, tokenID string) {
2952	if !unsafe.OriginSend().IsZero() {
2953		panic("garden: wake accepts no coins; switching which gnome is awake is free, once per UTC day")
2954	}
2955
2956	if tokenID == genesisTokenID {
2957		panic("garden: the genesis gnome belongs to nobody and can never be woken")
2958	}
2959
2960	caller := cur.Previous().Address()
2961	if !caller.IsValid() {
2962		panic("garden: wake requires a valid caller address")
2963	}
2964
2965	owner, ok := ownerOfToken(tokenID)
2966	if !ok || owner != caller {
2967		panic("garden: you do not own that gnome")
2968	}
2969
2970	if active, hasActive := activeToken.Get(caller.String()).(string); hasActive && active == tokenID {
2971		panic("garden: that gnome is already awake")
2972	}
2973
2974	now := time.Now().Unix()
2975	today := dayNumber(now)
2976	if last, isInt := lastWakeDay.Get(caller.String()).(int64); isInt && last == today {
2977		panic("garden: switching which gnome is awake is limited to once per UTC day; try again tomorrow")
2978	}
2979
2980	// g must exist: ownerOfToken above already confirmed tokenID is a real, owned token.
2981	// Recording lastWokenAt HERE, on the token actually being woken, is the other half of the
2982	// fix for ATTACK-REVIEW.md goal 7 -- see the gnome struct's own comment on the field. The
2983	// token being put to sleep (whichever was active(caller) a moment ago, if any) needs no
2984	// symmetrical write: its own lastActionAt/caredDayCount are already frozen facts the instant
2985	// it stops being the active one (SaveDay can only ever act on activeGnomeForAddr's result),
2986	// and the next time anything reactivates IT, that reactivation (another Wake, a later Buy by
2987	// a new owner, etc.) will set its lastWokenAt correctly at that time, exactly as this call is
2988	// doing now.
2989	g, ok := gnomeByTokenID(tokenID)
2990	if !ok {
2991		panic("garden: that gnome does not exist")
2992	}
2993	g.lastWokenAt = now
2994	gnomes.Set(tokenID, g)
2995
2996	activeToken.Set(caller.String(), tokenID)
2997	lastWakeDay.Set(caller.String(), today)
2998
2999	chain.Emit("Wake", "owner", caller.String(), "tokenId", tokenID)
3000}
3001
3002// ============================================================================
3003// SaveDay: v4's shape, exactly, resolved through the caller's ACTIVE gnome rather than "the"
3004// gnome for that address -- because after mint one/hold many/play one, there no longer is one.
3005// A sleeping gnome is simply never found here: activeGnomeForAddr only ever returns the awake
3006// token, so care applied to "my gnome" always means the one gnome actually allowed to earn
3007// anything (MINT.md section 2a: "a sleeping gnome earns zero -- no cared days, no streak, no
3008// level, no XP").
3009// ============================================================================
3010
3011type saveAction struct {
3012	kind string
3013	at   int64
3014	plot int
3015}
3016
3017func SaveDay(cur realm, actionsRaw string) {
3018	if !unsafe.OriginSend().IsZero() {
3019		panic("garden: save day accepts no coins; only Buy takes payment, and nothing else here can return them")
3020	}
3021
3022	caller := cur.Previous().Address()
3023	if !caller.IsValid() {
3024		panic("garden: save day requires a valid caller address")
3025	}
3026
3027	tid, g, ok := activeGnomeForAddr(caller)
3028	if !ok {
3029		panic("garden: no gnome is awake for this address; claim or buy one, or Wake one of your own")
3030	}
3031
3032	now := time.Now().Unix()
3033	actions := parseActions(actionsRaw)
3034
3035	if len(actions) == 0 {
3036		chain.Emit("SaveDay", "owner", caller.String(), "tokenId", tid, "actions", "0", "xpApplied", "0")
3037		return
3038	}
3039
3040	earliestAllowed := now - maxBackfillHours*hourSeconds
3041
3042	// awakeSince: the moment THIS token last became the active one (set by settleCore, Buy, and
3043	// Wake -- see the gnome struct's own comment). ATTACK-REVIEW.md goal 7: without this, a
3044	// token's own earliestAllowed was bounded only by its own lastActionAt and the 48-hour
3045	// horizon, with nothing checking that the backfilled window actually fell inside a period
3046	// this specific token was awake. A token that just became active (whether by a brand-new
3047	// Claim/Settle, a Buy, or an explicit Wake switching away from a sibling in the same stable)
3048	// cannot backfill anything from before that moment -- it was asleep then, and MINT.md /
3049	// DECISIONS.md are both explicit that a sleeping gnome earns zero, full stop, not "zero
3050	// unless a sibling token happens to cover the gap."
3051	awakeSince := g.lastWokenAt
3052	if awakeSince > earliestAllowed {
3053		earliestAllowed = awakeSince
3054	}
3055
3056	firstDay := dayNumber(actions[0].at)
3057	prevAt := g.lastActionAt
3058	if prevAt < earliestAllowed {
3059		prevAt = earliestAllowed
3060	}
3061	for _, a := range actions {
3062		if a.at <= earliestAllowed {
3063			if a.at <= awakeSince {
3064				panic("garden: this action predates the last time this gnome was woken; a sleeping gnome earns nothing for the time it was asleep")
3065			}
3066			panic("garden: this action is more than " + strconv.FormatInt(maxBackfillHours, 10) + " hours old; care can only be backfilled for the last " + strconv.FormatInt(maxBackfillHours, 10) + " hours, not longer absences")
3067		}
3068		if a.at <= prevAt {
3069			panic("garden: action timestamp must be strictly after the last saved action")
3070		}
3071		if a.at > now {
3072			panic("garden: action timestamp cannot be in the future")
3073		}
3074		if dayNumber(a.at) != firstDay {
3075			panic("garden: every action in one SaveDay call must fall on the same UTC day")
3076		}
3077		prevAt = a.at
3078	}
3079
3080	work := *g
3081	acorns := balanceOf(caller)
3082	spentThisCall := false
3083
3084	gateWaived := acorns < acornReserve && !anyPlotPlanted(g)
3085	rawCareXp := int64(0)
3086	tinkeredThisCall := false
3087
3088	for _, a := range actions {
3089		switch a.kind {
3090		case actionPat:
3091			if !canPat(work.lastPatAt, a.at, patCooldownHours) {
3092				panic("garden: pat must be at least " + strconv.FormatInt(patCooldownHours, 10) + " hours after the last one")
3093			}
3094			work.lastPatAt = a.at
3095			work.lastHappyEventAt = a.at
3096			work.cheer = clampMeter(meterNow(work.cheer, work.cheerAt, a.at, cheerDropEveryHours) + patCheer)
3097			work.cheerAt = a.at
3098			rawCareXp += xpPat
3099
3100		case actionFeed:
3101			if acorns < feedCost {
3102				panic("garden: not enough acorns to feed")
3103			}
3104			acorns -= feedCost
3105			spentThisCall = true
3106			work.belly = clampMeter(meterNow(work.belly, work.bellyAt, a.at, bellyDropEveryHours) + feedBelly)
3107			work.bellyAt = a.at
3108			work.lastHappyEventAt = a.at
3109			rawCareXp += xpFeed
3110
3111		case actionPlant:
3112			waived := gateWaived && acorns < acornReserve
3113			if !waived && meterNow(work.belly, work.bellyAt, a.at, bellyDropEveryHours) <= hungryAtOrBelow {
3114				panic("garden: too hungry to garden; feed first")
3115			}
3116			p := &work.plots[a.plot]
3117			if p.plantedAt != 0 {
3118				panic("garden: that plot is already planted")
3119			}
3120			if acorns < plantCost {
3121				panic("garden: not enough acorns to plant")
3122			}
3123			acorns -= plantCost
3124			spentThisCall = true
3125			p.plantedAt = a.at
3126			p.readyAt = a.at + growHours*hourSeconds
3127
3128		case actionHarvest:
3129			p := &work.plots[a.plot]
3130			if p.plantedAt == 0 {
3131				panic("garden: that plot has nothing to harvest")
3132			}
3133			if a.at < p.readyAt {
3134				panic("garden: that plot is not ready to harvest yet")
3135			}
3136			p.plantedAt = 0
3137			p.readyAt = 0
3138			acorns += harvestPerPlot
3139			work.xpGarden += xpHarvestPerPlot
3140			work.harvestCount++
3141			rawCareXp += xpHarvestPerPlot
3142
3143		case actionTinker:
3144			if tinkeredThisCall {
3145				panic("garden: only one tinker task per day")
3146			}
3147			if work.lastTinkerAt != 0 && dayNumber(work.lastTinkerAt) == dayNumber(a.at) {
3148				panic("garden: already did a tinker task today")
3149			}
3150			if acorns < tinkerCost {
3151				panic("garden: not enough acorns to tinker")
3152			}
3153			acorns -= tinkerCost
3154			spentThisCall = true
3155			work.xpTinker += xpTinkerTask
3156			work.lastTinkerAt = a.at
3157			tinkeredThisCall = true
3158			rawCareXp += xpTinkerTask
3159
3160		default:
3161			panic("garden: unknown action kind")
3162		}
3163	}
3164
3165	if spentThisCall && acorns < acornReserve && !anyPlotPlanted(&work) {
3166		panic("garden: that would leave fewer than " + strconv.FormatInt(acornReserve, 10) + " acorns with nothing planted, and a gnome with no acorns and no garden can never earn another; plant first, or keep " + strconv.FormatInt(acornReserve, 10) + " back")
3167	}
3168
3169	latestActionAt := actions[len(actions)-1].at
3170	bellyAtEnd := meterNow(work.belly, work.bellyAt, latestActionAt, bellyDropEveryHours)
3171	mood := moodNow(bellyAtEnd, work.lastPatAt, work.lastHappyEventAt, work.adoptedAt, latestActionAt, hungryAtOrBelow)
3172
3173	adjustedXp := rawCareXp
3174	switch mood {
3175	case moodHappy:
3176		adjustedXp = adjustedXp * (100 + happyBonusPct) / 100
3177	case moodSulky:
3178		adjustedXp = adjustedXp * (100 - sulkyPenaltyPct) / 100
3179	}
3180
3181	if work.dailyCareDay != firstDay {
3182		work.dailyCareDay = firstDay
3183		work.dailyCareXp = 0
3184	}
3185	remainingToday := dailyCareCap - work.dailyCareXp
3186	if remainingToday < 0 {
3187		remainingToday = 0
3188	}
3189	if adjustedXp > remainingToday {
3190		adjustedXp = remainingToday
3191	}
3192	work.dailyCareXp += adjustedXp
3193
3194	work.totalXp += adjustedXp
3195	work.level = levelFromXp(work.totalXp)
3196
3197	// At most ONE of this owner's tokens may ever bank a given UTC day as cared-for -- see
3198	// lastCaredDayForOwner's own comment. Without this, Wake (once per day, but still once)
3199	// lets an owner genuinely care for A earlier today, switch to B, and genuinely care for B
3200	// too, banking the SAME calendar day on both from nothing but real, non-backfilled
3201	// timestamps -- the live-switch half of ATTACK-REVIEW.md goal 7 that lastWokenAt's backfill
3202	// floor does not by itself prevent. XP, acorns, feeding, planting and harvesting are
3203	// deliberately NOT gated here: those are this token's own resource mechanics (already
3204	// bounded per-gnome by dailyCareCap) and giving B real care today is still real care: only
3205	// the CARED-DAY LEDGER -- the thing lockCaredDays and the public streak actually count -- is
3206	// restricted to one credit per owner per day.
3207	if lastOwnerDay, ok := lastCaredDayForOwner.Get(caller.String()).(int64); !ok || lastOwnerDay != firstDay {
3208		if recordCaredDay(&work, firstDay) {
3209			lastCaredDayForOwner.Set(caller.String(), firstDay)
3210		}
3211	}
3212	work.lastActionAt = latestActionAt
3213
3214	setBalance(caller, acorns)
3215	*g = work
3216	gnomes.Set(tid, g)
3217
3218	chain.Emit(
3219		"SaveDay",
3220		"owner", caller.String(),
3221		"tokenId", tid,
3222		"actions", strconv.Itoa(len(actions)),
3223		"xpApplied", strconv.FormatInt(adjustedXp, 10),
3224		"mood", mood,
3225	)
3226}
3227
3228// ============================================================================
3229// ChooseType: v4's shape, resolved through the active gnome.
3230// ============================================================================
3231
3232func ChooseType(cur realm, kind string) {
3233	if !unsafe.OriginSend().IsZero() {
3234		panic("garden: choose type accepts no coins; only Buy takes payment, and nothing else here can return them")
3235	}
3236
3237	caller := cur.Previous().Address()
3238	if !caller.IsValid() {
3239		panic("garden: choose type requires a valid caller address")
3240	}
3241
3242	tid, g, ok := activeGnomeForAddr(caller)
3243	if !ok {
3244		panic("garden: no gnome is awake for this address")
3245	}
3246
3247	if g.gnomeType != "" {
3248		panic("garden: this gnome's calling is already chosen and cannot be changed")
3249	}
3250
3251	level := levelFromXp(g.totalXp)
3252	if level < typeAtLevel {
3253		panic("garden: a calling can only be chosen at level " + strconv.FormatInt(typeAtLevel, 10) + " or above")
3254	}
3255
3256	if kind != typeGardener && kind != typeExplorer && kind != typeTinkerer {
3257		panic("garden: a calling is one of \"" + typeGardener + "\", \"" + typeExplorer + "\" or \"" + typeTinkerer + "\"")
3258	}
3259
3260	work := *g
3261	work.gnomeType = kind
3262	work.level = level
3263	*g = work
3264	gnomes.Set(tid, g)
3265
3266	chain.Emit("ChooseType", "owner", caller.String(), "tokenId", tid, "type", kind)
3267}
3268
3269// ============================================================================
3270// v6 FIX 1: SetName. THE ONLY NEW WRITE SURFACE IN THIS WHOLE FILE RELATIVE TO v5.
3271//
3272// WHY IT IS A SEPARATE FUNCTION AND NOT A PARAMETER ON Claim OR Settle, which is the first
3273// thing a reader will wonder and the one design question here that is already settled: the mint
3274// is a ROLL, and the whole safety property of the two-phase roll (MINT.md section 10.5, and this
3275// file's own package doc at length) is that the claimant does not and cannot choose what comes
3276// out of it. A name parameter on Claim would not technically break that -- a name does not
3277// influence the draw -- but it would blur the one sentence the product has to be able to say
3278// without an asterisk ("you do not choose what you get"), and it would mean a claimant has to
3279// have decided on a name at the exact moment they are least able to: before they have any idea
3280// what they are naming. Naming what you GOT is a different act from getting it, so it is a
3281// different function, called later, by the owner, as many times as they like.
3282//
3283// ONLY THE CURRENT OWNER. Not the original claimant -- the CURRENT owner. A gnome that has been
3284// sold belongs to its buyer, and so does its name; the seller loses the ability to rename it at
3285// the same instant they lose the token, through the same ownerOfToken check every other
3286// owner-gated function in this file uses (Wake, List). There is no admin override: this realm's
3287// admin can gate issuance and nothing else (see AdminAllow's own comment), and a name is not
3288// issuance.
3289// ============================================================================
3290
3291// SetName names tokenID, which the caller must currently own, or RENAMES it. Free, unlimited,
3292// and refused for the eight named one-of-ones and the genesis gnome.
3293//
3294// RENAMING IS ALLOWED, NOT ONE-SHOT, and this is a deliberate decision with a named trade-off
3295// rather than an oversight. On an immutable realm a one-shot name means a typo is forever, and
3296// "my gnome is called Fenncik" is a worse permanent outcome than "somebody changed their mind".
3297// Aavegotchi, the closest prior art this project has looked at, allows renaming. What it costs:
3298// a name is not a stable identifier over time, so nothing anywhere -- in this realm, in the
3299// adventure realm, or in the app -- may ever key anything on a name. tokenID is the identifier;
3300// the name is a label. (The nameOwner index is keyed BY name only to answer "is this taken",
3301// never to identify a gnome; every function here looks tokens up by tokenID.)
3302//
3303// Refuses coins for the same reason every other non-paying function here does: there is no
3304// banker to return them, and naming your own gnome is free (MINT.md section 14 -- batch one is
3305// free end to end, and this is not a new place to charge).
3306func SetName(cur realm, tokenID string, name string) {
3307	if !unsafe.OriginSend().IsZero() {
3308		panic("garden: set name accepts no coins; naming your own gnome is free, and nothing here can return them")
3309	}
3310
3311	// Checked FIRST and by name, before the ownership lookup that would also refuse it: the
3312	// genesis gnome belongs to nobody, so there is no caller for whom the owner check could ever
3313	// pass, but saying so explicitly is this file's established discipline for it (List, Buy and
3314	// Wake all do the same) and it gives a reader of the panic the real reason rather than a
3315	// misleading "you do not own that gnome".
3316	if tokenID == genesisTokenID {
3317		panic("garden: the genesis gnome belongs to nobody and can never be renamed")
3318	}
3319
3320	caller := cur.Previous().Address()
3321	if !caller.IsValid() {
3322		panic("garden: set name requires a valid caller address")
3323	}
3324
3325	owner, ok := ownerOfToken(tokenID)
3326	if !ok || owner != caller {
3327		panic("garden: you do not own that gnome")
3328	}
3329
3330	g, isGnome := gnomeByTokenID(tokenID)
3331	if !isGnome {
3332		panic("garden: that tokenID does not exist")
3333	}
3334
3335	// THE EIGHT NAMED ONE-OF-ONES KEEP THEIR CHARACTER NAME, PERMANENTLY. Hob is Hob; that is
3336	// the entire point of a cap-one edition, and an owner who could rename Hob to "xXx" would be
3337	// destroying the thing they were lucky enough to roll, irreversibly, for everyone who comes
3338	// after them. This also closes the obvious way around the reservation: if a named edition
3339	// could be renamed, its own canonical key would be freed by the rename (SetName's commit
3340	// block frees the old key by design -- see nameOwner's comment) and "hob" would become
3341	// takeable by an ordinary gnome even though reservedNameKeys still lists it, because
3342	// reservation is checked against the NEW name, never the freed one. Refusing the rename
3343	// outright is what keeps those two mechanisms from interacting.
3344	if g.character != "" {
3345		panic("garden: " + g.character + " is a named one-of-one and keeps its own name permanently; only ordinary gnomes can be named")
3346	}
3347
3348	// A LISTED GNOME CANNOT BE RENAMED, because the seller still owns it right up until the
3349	// instant Buy lands and every guard above would therefore pass. THE ATTACK: Alice names
3350	// token A "Pebble" and lists it. Bob reads the name, broadcasts Buy(A) with the full price.
3351	// Before Bob's transaction lands, Alice calls SetName(A, "Junk") -- she is still the owner,
3352	// so nothing refuses her -- and then SetName(B, "Pebble") on a second token she keeps, which
3353	// the rename just freed the canonical key for (see the commit block below: the old key is
3354	// released by design). Bob's Buy still succeeds, at the agreed price, for a gnome now called
3355	// "Junk", and "Pebble" is permanently out of his reach because names are unique and
3356	// first-come. Buy cannot defend itself here: it never reads g.name and takes no expected-name
3357	// argument, so there is nothing for it to compare. Refusing the rename while a listing stands
3358	// is what makes the name a buyer reads part of what they are actually buying.
3359	//
3360	// The test is listingFor, the SAME predicate Cancel uses for "is this gnome for sale" -- not
3361	// a second notion of "listed" that could drift out of step with the marketplace's own. It is
3362	// deliberately presence-based rather than presence-plus-unexpired: an expired listing is
3363	// unbuyable (Buy refuses it) so renaming under one is harmless, but copying Buy's inline
3364	// expiry comparison here would be a second copy of a liveness rule that could later drift,
3365	// and refusing slightly too much costs the owner one Cancel call while refusing slightly too
3366	// little reopens the hole on an immutable realm.
3367	if _, isListed := listingFor(tokenID); isListed {
3368		panic("garden: that gnome is listed for sale and cannot be renamed while a buyer may already have a purchase in flight; cancel the listing first, then rename it")
3369	}
3370
3371	// All validation and every refusal happens in here, before any write below.
3372	assertNameAssignable(name, tokenID)
3373
3374	// ---- commit. Nothing below this line can fail. ----
3375
3376	// FREE THE OLD KEY BEFORE TAKING THE NEW ONE. Required, not tidiness -- see nameOwner's own
3377	// comment: without this, one token could hoard every name in the collection by renaming
3378	// repeatedly. Order matters for one specific case: renaming a gnome to a different
3379	// capitalisation of the name it already has ("fennick" -> "Fennick") canonicalises to the
3380	// SAME key, so removing first and setting second is the only order that does not either
3381	// delete the key it just wrote or refuse a legal, harmless re-capitalisation.
3382	if g.name != "" {
3383		nameOwner.Remove(canonicalNameKey(g.name))
3384	}
3385	nameOwner.Set(canonicalNameKey(name), tokenID)
3386
3387	previous := g.name
3388	g.name = name
3389	gnomes.Set(tokenID, g)
3390
3391	chain.Emit(
3392		"SetName",
3393		"owner", caller.String(),
3394		"tokenId", tokenID,
3395		"name", name,
3396		"previousName", previous,
3397	)
3398}
3399
3400// assertNameAssignable is SetName's refusal gate: it panics with nameAssignabilityProblem's
3401// message, or returns normally if name may be assigned to tokenID. The rules themselves are in
3402// nameAssignabilityProblem, not here, so the free NameAvailability query and this enforcing path
3403// can never disagree about what is allowed -- there is exactly one copy.
3404func assertNameAssignable(name string, tokenID string) {
3405	if problem := nameAssignabilityProblem(name, tokenID); problem != "" {
3406		panic(problem)
3407	}
3408}
3409
3410// nameAssignabilityProblem is THE one copy of the naming rules. It returns "" if name may be
3411// assigned to tokenID, or the exact player-meaningful message explaining which rule refused it.
3412//
3413// IT RETURNS ITS REFUSAL RATHER THAN PANICKING IT so that NameAvailability can offer the same
3414// answer as a free read without any recover() -- see NameAvailability's own note on why recover
3415// is avoided on this toolchain.
3416//
3417// ORDER IS DELIBERATE AND MATCHES app/js/config.js's nameError, which mirrors v4's realm order:
3418// length, then charset, then visible content, then (new in v6) reservation, then uniqueness. A
3419// name that fails several rules reports the first, so the app's own message and the realm's
3420// refusal always name the SAME rule for the same input rather than disagreeing about which
3421// problem is the problem.
3422func nameAssignabilityProblem(name string, tokenID string) string {
3423	if !validNameLength(name) {
3424		return "garden: name must be 1-" + strconv.Itoa(maxNameLen) + " characters"
3425	}
3426
3427	// Checked before the general charset refusal purely so the message is useful: iOS "smart
3428	// punctuation" is ON BY DEFAULT and silently rewrites a straight apostrophe to U+2018/U+2019
3429	// and a hyphen to U+2013/U+2014 (app/js/config.js's third audit round, finding Y8). A player
3430	// who typed "O'Hare" on an iPhone did not type anything strange, and telling them their name
3431	// "contains a character that is not an allowed letter" would be true but useless.
3432	//
3433	// THIS IS A REFUSAL, NOT A TRANSFORM, AND THAT IS THE POINT. config.js's normalizeName folds
3434	// those four code points back to ASCII in the BROWSER, before submitting -- a narrowing
3435	// transform whose own comment notes "the realm remains the sole authority". This realm
3436	// deliberately does NOT fold them itself: a name is now a reserved, unique, first-come
3437	// resource, so silently rewriting what somebody signed for would mean the key they got is not
3438	// the key they asked for, and two players could race for a name neither of them typed.
3439	// Refusing and naming the reason keeps the realm's accepted set exactly as narrow as v4's
3440	// while still telling an iPhone player what happened.
3441	if hasSmartPunctuation(name) {
3442		return "garden: name contains curly quotes or dashes, which a phone keyboard may have substituted for a plain apostrophe or hyphen; retype it with a straight ' or -"
3443	}
3444
3445	if !validNameCharset(name) {
3446		return "garden: name contains a character that is not an allowed letter, digit, space, hyphen or apostrophe"
3447	}
3448
3449	if !hasVisibleNameContent(name) {
3450		return "garden: name must contain at least one visible character"
3451	}
3452
3453	key := canonicalNameKey(name)
3454
3455	// RESERVATION. An ordinary gnome may not be named after a cap-one edition or the genesis
3456	// gnome, whether or not that edition has minted yet -- see reservedNameKeys' own comment for
3457	// why this is not already covered by the uniqueness check below.
3458	for _, reserved := range reservedNameKeys {
3459		if key == reserved {
3460			return "garden: \"" + name + "\" is reserved for a named one-of-one and cannot be taken by an ordinary gnome"
3461		}
3462	}
3463
3464	// UNIQUENESS. Checked against the CANONICAL key, so case and spacing variants of a taken name
3465	// are refused too -- the cheapest impersonation attempt there is.
3466	//
3467	// The `holder != tokenID` test is what makes a HARMLESS self-rename legal: re-capitalising
3468	// your own gnome's own name ("fennick" -> "Fennick") canonicalises to a key you already hold,
3469	// and refusing that as "already taken" would be refusing you your own name. Callers who want
3470	// the general "is this free for anybody" question pass tokenID "" (NameAvailability's own
3471	// documented shape), which can never equal a real tokenID, so nothing is accidentally
3472	// exempted by it.
3473	if holder, taken := nameOwnerOf(key); taken && holder != tokenID {
3474		return "garden: the name \"" + name + "\" is already taken by gnome " + holder + "; names are unique across the collection"
3475	}
3476
3477	return ""
3478}
3479
3480// ============================================================================
3481// SALE: List/Cancel/Buy. v4's shape, DESIGN.md section 9's reasoning unchanged, minus the
3482// one-gnome-per-address refusal in Buy (MINT.md section 2a: removed from Buy, never from Claim)
3483// and plus a tokenID parameter on List/Cancel, because an owner may now have more than one
3484// gnome to choose among, and plus the lockCaredDays check on the eight named one-of-ones.
3485// ============================================================================
3486
3487// List offers tokenID, which the caller must own, for sale. The lock: a named one-of-one
3488// (character != "") cannot be listed until it has lockCaredDays cared days; an ordinary gnome
3489// has no such restriction, trading from the moment it is minted (MINT.md section 12/13).
3490func List(cur realm, tokenID string, priceUgnot int64, buyer address, days int64) {
3491	if !unsafe.OriginSend().IsZero() {
3492		panic("garden: list accepts no coins; only Buy takes payment")
3493	}
3494
3495	if tokenID == genesisTokenID {
3496		panic("garden: the genesis gnome belongs to nobody and can never be listed")
3497	}
3498
3499	caller := cur.Previous().Address()
3500	if !caller.IsValid() {
3501		panic("garden: list requires a valid caller address")
3502	}
3503
3504	owner, ok := ownerOfToken(tokenID)
3505	if !ok || owner != caller {
3506		panic("garden: you do not own that gnome")
3507	}
3508
3509	gv := gnomes.Get(tokenID)
3510	g, isGnome := gv.(*gnome)
3511	if !isGnome || g == nil {
3512		panic("garden: that tokenID does not exist")
3513	}
3514
3515	if g.character != "" && g.caredDayCount < lockCaredDays {
3516		// THIS MESSAGE SAYS THE ROSTER SIZE OUT LOUD, which makes it the FOURTH place v7 change 10
3517		// had to edit (after namedEditionOrder, namedEditionDisplayNames and reservedNameKeys) and
3518		// the one the existing comments did not warn about. The test suite's lockedMsg helper
3519		// mirrors this string byte-for-byte, so a change here that is not made there fails
3520		// TestNamedOneOfOneCannotBeListedBeforeTheLockClears rather than shipping.
3521		panic("garden: " + g.character + " is one of the eight named one-of-ones and cannot be listed until it has " +
3522			strconv.FormatInt(lockCaredDays, 10) + " cared days; it has " + strconv.FormatInt(g.caredDayCount, 10))
3523	}
3524
3525	if priceUgnot < 0 {
3526		panic("garden: a price cannot be negative")
3527	}
3528	if priceUgnot > maxPriceUgnot {
3529		panic("garden: price exceeds the maximum of " + strconv.FormatInt(maxPriceUgnot, 10) + " ugnot")
3530	}
3531	if days < 1 || days > maxListingDays {
3532		panic("garden: a listing lasts between 1 and " + strconv.FormatInt(maxListingDays, 10) + " days")
3533	}
3534
3535	buyer = address(lowerASCII(string(buyer)))
3536	if buyer != address("") {
3537		if !buyer.IsValid() {
3538			panic("garden: reserved buyer is not a valid address; pass an empty address to let anyone buy")
3539		}
3540		if buyer == caller {
3541			panic("garden: you cannot reserve your own gnome for yourself")
3542		}
3543	}
3544
3545	now := time.Now().Unix()
3546	listings.Set(tokenID, &listing{
3547		seller:     caller,
3548		priceUgnot: priceUgnot,
3549		buyer:      buyer,
3550		listedAt:   now,
3551		expiresAt:  now + days*daySeconds,
3552	})
3553
3554	feeUgnot := feeFor(priceUgnot)
3555	chain.Emit(
3556		"List",
3557		"seller", caller.String(),
3558		"tokenId", tokenID,
3559		"priceUgnot", strconv.FormatInt(priceUgnot, 10),
3560		"feeUgnot", strconv.FormatInt(feeUgnot, 10),
3561		"totalUgnot", strconv.FormatInt(priceUgnot+feeUgnot, 10),
3562		"buyer", buyer.String(),
3563		"expiresAt", strconv.FormatInt(now+days*daySeconds, 10),
3564	)
3565}
3566
3567// Cancel withdraws the caller's own listing on tokenID. Only the seller who created it may.
3568func Cancel(cur realm, tokenID string) {
3569	if !unsafe.OriginSend().IsZero() {
3570		panic("garden: cancel accepts no coins; only Buy takes payment")
3571	}
3572
3573	caller := cur.Previous().Address()
3574	if !caller.IsValid() {
3575		panic("garden: cancel requires a valid caller address")
3576	}
3577
3578	l, ok := listingFor(tokenID)
3579	if !ok {
3580		panic("garden: that gnome is not for sale")
3581	}
3582	if l.seller != caller {
3583		panic("garden: only the seller can cancel their own listing")
3584	}
3585
3586	listings.Remove(tokenID)
3587	chain.Emit("Cancel", "seller", caller.String(), "tokenId", tokenID)
3588}
3589
3590// Buy purchases the listed gnome tokenID. v4's validate-then-commit shape, unchanged, MINUS the
3591// one-gnome-per-address refusal: MINT.md section 2a removes the hold cap from Buy specifically,
3592// and only from Buy -- Claim's hasRolled gate is the cap that still matters, and this function
3593// never touches it.
3594func Buy(cur realm, tokenID string) {
3595	if !cur.Previous().IsUserCall() {
3596		panic("garden: buy must be called directly by a wallet, not through another realm; the coins attached to a transaction belong to the realm the wallet called")
3597	}
3598	buyer := cur.Previous().Address()
3599	if !buyer.IsValid() {
3600		panic("garden: buy requires a valid caller address")
3601	}
3602
3603	if tokenID == genesisTokenID {
3604		panic("garden: the genesis gnome belongs to nobody and can never be bought")
3605	}
3606
3607	l, ok := listingFor(tokenID)
3608	if !ok {
3609		panic("garden: that gnome is not for sale")
3610	}
3611
3612	now := time.Now().Unix()
3613	if now > l.expiresAt {
3614		panic("garden: that listing has expired; the seller must list again")
3615	}
3616	if l.seller == buyer {
3617		panic("garden: you cannot buy your own gnome")
3618	}
3619	if l.buyer != address("") && l.buyer != buyer {
3620		panic("garden: that gnome is reserved for a different buyer")
3621	}
3622
3623	tid := grc721.TokenID(tokenID)
3624	owner, err := token.OwnerOf(tid)
3625	if err != nil || owner != l.seller {
3626		panic("garden: that listing is stale; the seller no longer owns that gnome")
3627	}
3628	gv := gnomes.Get(tokenID)
3629	g, isGnome := gv.(*gnome)
3630	if !isGnome || g == nil {
3631		panic("garden: that listing names a gnome this realm does not hold")
3632	}
3633
3634	feeUgnot := feeFor(l.priceUgnot)
3635	total := l.priceUgnot + feeUgnot
3636	send := unsafe.OriginSend()
3637	if !exactUgnot(send, total) {
3638		panic("garden: buy requires exactly " + strconv.FormatInt(total, 10) + ugnotDenom +
3639			" attached (" + strconv.FormatInt(l.priceUgnot, 10) + " price + " +
3640			strconv.FormatInt(feeUgnot, 10) + " fee); got " + coinsString(send))
3641	}
3642
3643	// ---- commit. Nothing below this line can fail on chain. ----
3644
3645	transferGate = tokenID
3646	if err := ledger.TransferFrom(l.seller, l.seller, buyer, tid); err != nil {
3647		panic(err)
3648	}
3649	transferGate = ""
3650
3651	removeOwnerToken(l.seller, tokenID)
3652	addOwnerToken(buyer, tokenID)
3653	listings.Remove(tokenID)
3654
3655	// Active-token bookkeeping (mint one, hold many, play one). If this was the seller's awake
3656	// gnome, they fall asleep entirely rather than another of their gnomes quietly becoming
3657	// active -- switching is always a deliberate Wake, never a side effect of a sale. If the
3658	// buyer currently has nothing awake at all, this purchase becomes their active gnome
3659	// automatically, so a first-time buyer is never left with nothing playable.
3660	if av, isActive := activeToken.Get(l.seller.String()).(string); isActive && av == tokenID {
3661		activeToken.Remove(l.seller.String())
3662	}
3663	if !activeToken.Has(buyer.String()) {
3664		activeToken.Set(buyer.String(), tokenID)
3665	}
3666
3667	settled := int64(0)
3668	for i := 0; i < numPlots; i++ {
3669		p := &g.plots[i]
3670		if p.plantedAt == 0 {
3671			continue
3672		}
3673		if now >= p.readyAt {
3674			settled += harvestPerPlot
3675		} else {
3676			settled += plantCost
3677		}
3678		p.plantedAt = 0
3679		p.readyAt = 0
3680	}
3681	if settled > 0 {
3682		setBalance(l.seller, balanceOf(l.seller)+settled)
3683	}
3684
3685	g.acquiredAt = now
3686	g.saleCount++
3687	g.lastActionAt = now
3688	// lastWokenAt resets here too, same as lastActionAt, whether or not this purchase becomes
3689	// the buyer's active gnome below -- if it does not (buyer already has something awake), this
3690	// value is simply overwritten by the eventual explicit Wake that does activate it. Either
3691	// way, nothing the SELLER (or anyone else) did with this token before the sale can be
3692	// backfilled by the BUYER afterward: SaveDay's floor cannot reach earlier than this moment.
3693	g.lastWokenAt = now
3694	gnomes.Set(tokenID, g)
3695
3696	grantStartingPurseIfNew(buyer)
3697
3698	bank := banker.NewBanker(banker.BankerTypeOriginSend, cur)
3699	me := cur.Address()
3700	if l.priceUgnot > 0 {
3701		bank.SendCoins(me, l.seller, chain.Coins{{Denom: ugnotDenom, Amount: l.priceUgnot}})
3702	}
3703	bank.SendCoins(me, fee.recipient, chain.Coins{{Denom: ugnotDenom, Amount: feeUgnot}})
3704
3705	chain.Emit(
3706		"Sale",
3707		"tokenId", tokenID,
3708		"seller", l.seller.String(),
3709		"buyer", buyer.String(),
3710		"priceUgnot", strconv.FormatInt(l.priceUgnot, 10),
3711		"feeUgnot", strconv.FormatInt(feeUgnot, 10),
3712		"feeRecipient", fee.recipient.String(),
3713		"settledAcorns", strconv.FormatInt(settled, 10),
3714	)
3715}
3716
3717// ============================================================================
3718// Readers
3719// ============================================================================
3720
3721func Listing(tokenID string) (seller address, priceUgnot int64, buyer address, listedAtUnix, expiresAtUnix, feeUgnot, totalUgnot int64, found bool) {
3722	l, ok := listingFor(tokenID)
3723	if !ok {
3724		return address(""), 0, address(""), 0, 0, 0, 0, false
3725	}
3726	fee := feeFor(l.priceUgnot)
3727	return l.seller, l.priceUgnot, l.buyer, l.listedAt, l.expiresAt, fee, l.priceUgnot + fee, true
3728}
3729
3730func TotalListings() int64 {
3731	return int64(listings.Size())
3732}
3733
3734func ListingsPage(offset, limit int) []string {
3735	if offset < 0 {
3736		panic("garden: offset must not be negative")
3737	}
3738	if limit <= 0 {
3739		panic("garden: limit must be positive")
3740	}
3741	if limit > maxListingsPageSize {
3742		limit = maxListingsPageSize
3743	}
3744	tokenIDs := make([]string, 0, limit)
3745	listings.IterateByOffset(offset, limit, func(key string, value any) bool {
3746		tokenIDs = append(tokenIDs, key)
3747		return false
3748	})
3749	return tokenIDs
3750}
3751
3752func QuoteFee(priceUgnot int64) (feeUgnot, totalUgnot int64) {
3753	f := feeFor(priceUgnot)
3754	return f, priceUgnot + f
3755}
3756
3757func FeeRules() string {
3758	return "saleFeeRateBps=" + strconv.FormatInt(fee.rateBps, 10) +
3759		";saleFeeFloorUgnot=" + strconv.FormatInt(fee.floorUgnot, 10) +
3760		";saleFeeRecipient=" + fee.recipient.String()
3761}
3762
3763func Provenance(tokenID string) (adoptedAtUnix, acquiredAtUnix, saleCount int64, found bool) {
3764	g, ok := gnomeByTokenID(tokenID)
3765	if !ok {
3766		return 0, 0, 0, false
3767	}
3768	return g.adoptedAt, g.acquiredAt, g.saleCount, true
3769}
3770
3771// ---- v6's new readers. FIVE additions, every one of them read-only. The existing readers'
3772// SIGNATURES ARE ALL UNCHANGED FROM v5 ON PURPOSE, including Provenance's and GnomeByToken's,
3773// even though the round and the name would both have fitted naturally into them: the app already
3774// reads v5's shapes, and widening a tuple return breaks every existing caller for no gain that a
3775// dedicated reader does not already give. Reads are free and unmetered, so "one more query" costs
3776// a caller nothing; a changed signature costs them a rewrite.
3777
3778// Name returns tokenID's name, or found=false if there is no such token.
3779//
3780// A name of "" is a REAL, EXPECTED ANSWER, not an error: it means this gnome has not been named
3781// yet, which is the state every ordinary gnome mints in. found reports whether the TOKEN exists,
3782// never whether it has a name -- check the string for that.
3783func Name(tokenID string) (name string, found bool) {
3784	g, ok := gnomeByTokenID(tokenID)
3785	if !ok {
3786		return "", false
3787	}
3788	return g.name, true
3789}
3790
3791// NameOwner resolves a name to the tokenID currently carrying it, matching the same way SetName
3792// refuses duplicates: on the canonical form, so any capitalisation or spacing of a taken name
3793// finds it. This is the uniqueness index read back, and the honest answer to "who is Fennick".
3794func NameOwner(name string) (tokenID string, found bool) {
3795	return nameOwnerOf(canonicalNameKey(name))
3796}
3797
3798// NameAvailability answers "would SetName accept this name, and if not why" WITHOUT a transaction
3799// -- a free query against the same assertNameAssignable the write path uses, so the realm itself
3800// is the authority the app checks against instead of a second copy of the rules that can drift
3801// from it. reason is "" when ok is true, and the realm's own refusal message otherwise.
3802//
3803// The tokenID argument matters for exactly one case: a name is available TO THE TOKEN THAT
3804// ALREADY HOLDS IT (re-capitalising your own gnome's name). Pass "" to ask the general question
3805// "is this name free for somebody new", which is what an app naming a fresh gnome wants.
3806//
3807// THIS IS A CONVENIENCE, NOT THE AUTHORITY, and the distinction is load-bearing: a true answer
3808// here can be made false by somebody else's transaction landing first. SetName re-checks
3809// everything itself, always -- this function cannot be used to skip a check, only to avoid
3810// wasting a player's gas on a refusal that was already knowable.
3811//
3812// NOTE ON SHAPE, because the obvious implementation is a trap on this toolchain: this does NOT
3813// call SetName's validator and recover() from its panic. `recover()` does not reliably catch a
3814// panic across a realm boundary in Gno (MEMORY.md records this costing this project real time),
3815// and a reader should not have to reason about whether a same-realm call is the exception. So the
3816// rules live in nameAssignabilityProblem, which RETURNS its refusal instead of panicking it, and
3817// both this reader and assertNameAssignable call that one copy. No recover anywhere.
3818func NameAvailability(name string, tokenID string) (ok bool, reason string) {
3819	reason = nameAssignabilityProblem(name, tokenID)
3820	return reason == "", reason
3821}
3822
3823// SettledRound returns the drand round whose published signature produced tokenID's parts --
3824// v6 FIX 2, and the one query that makes this project's "provably unrigged" claim (ECONOMY.md
3825// section 1) checkable by a holder rather than merely asserted at them. Fetch
3826// https://api.drand.sh/<chain>/public/<round>, hash its signature, and re-derive the parts with
3827// this file's own seedFor/drawWeighted against your own address: a round signed by drand before
3828// anybody knew who would pin it cannot have been chosen to produce a particular gnome.
3829//
3830// round == 0 means this token was never produced by a roll, which is true of exactly one token
3831// ever: the genesis gnome. found reports whether the token exists.
3832func SettledRound(tokenID string) (round int64, found bool) {
3833	g, ok := gnomeByTokenID(tokenID)
3834	if !ok {
3835		return 0, false
3836	}
3837	return g.settledRound, true
3838}
3839
3840// MaxNameLen is maxNameLen read back, so the app can show the limit it will be held to instead of
3841// hardcoding a second copy of the number (app/js/config.js's NAME_MAX_LEN is the other copy, and
3842// the two are kept equal by hand and by TestMaxNameLenMatchesTheAppsRule).
3843func MaxNameLen() int64 { return int64(maxNameLen) }
3844
3845// GnomeByToken: the basic facts for one token -- hat, coat, eyes, nose, character (empty for an
3846// ordinary gnome), level, calling and the moment it was minted. SIGNATURE UNCHANGED FROM v5 --
3847// the name is Name(tokenID) and the round is SettledRound(tokenID); see the note above.
3848func GnomeByToken(tokenID string) (hat, coat string, eyes, nose int64, character string, level int64, gnomeType string, adoptedAtUnix int64, found bool) {
3849	g, ok := gnomeByTokenID(tokenID)
3850	if !ok {
3851		return "", "", 0, 0, "", 0, "", 0, false
3852	}
3853	return g.hat, g.coat, g.eyes, g.nose, g.character, g.level, g.gnomeType, g.adoptedAt, true
3854}
3855
3856// ActiveGnome returns the tokenID that is awake for addr right now, or found=false if nothing
3857// is (never claimed or bought a gnome, or sold the only one and has not Woken another since).
3858func ActiveGnome(addr address) (tokenID string, found bool) {
3859	tid, isStr := activeToken.Get(addr.String()).(string)
3860	if !isStr || tid == "" {
3861		return "", false
3862	}
3863	return tid, true
3864}
3865
3866// OwnerTokens returns every tokenID addr currently holds, in no particular order. Unbounded and
3867// unpaginated deliberately: batchCap is 60 (v7's own number -- v5 and v6 argued this at 24) and
3868// the genesis gnome is permanently unreachable to any wallet, so no address can ever hold more
3869// than batchCap tokens. Sixty token ids in one return is still a small, fixed,
3870// compile-time-bounded list well inside what a single read returns. THE NUMBER IN THIS COMMENT
3871// IS PART OF THE DELIVERABLE: a false comment on an immutable realm is permanent.
3872func OwnerTokens(addr address) []string {
3873	v := ownerTokens.Get(addr.String())
3874	list, ok := v.(*[]string)
3875	if !ok || list == nil {
3876		return []string{}
3877	}
3878	out := make([]string, len(*list))
3879	copy(out, *list)
3880	return out
3881}
3882
3883// TokensPage: the global token enumerator, hollowmere/v1's own pattern (TotalClaimants/
3884// ClaimantsPage) applied here, because v4's own missing-enumerator lesson (DECISIONS.md:780-791,
3885// arena/v1 shipping with no challenge inbox and no way to ever add one) is unavailable to fix
3886// after deploy and twenty lines now is cheaper than "unavailable forever" later.
3887func TokensPage(offset, limit int) []string {
3888	if offset < 0 {
3889		panic("garden: offset must not be negative")
3890	}
3891	if limit <= 0 {
3892		panic("garden: limit must be positive")
3893	}
3894	if limit > maxGnomesPageSize {
3895		limit = maxGnomesPageSize
3896	}
3897	ids := make([]string, 0, limit)
3898	gnomes.IterateByOffset(offset, limit, func(key string, value any) bool {
3899		ids = append(ids, key)
3900		return false
3901	})
3902	return ids
3903}
3904
3905func GardenState(tokenID string) (plantedAtCsv, readyAtCsv string, readyCount int64, found bool) {
3906	g, ok := gnomeByTokenID(tokenID)
3907	if !ok {
3908		return "", "", 0, false
3909	}
3910	now := time.Now().Unix()
3911	planted := make([]string, 0, numPlots)
3912	ready := make([]string, 0, numPlots)
3913	for i := 0; i < numPlots; i++ {
3914		p := g.plots[i]
3915		planted = append(planted, strconv.FormatInt(p.plantedAt, 10))
3916		ready = append(ready, strconv.FormatInt(p.readyAt, 10))
3917		if p.plantedAt != 0 && now >= p.readyAt {
3918			readyCount++
3919		}
3920	}
3921	return strings.Join(planted, ","), strings.Join(ready, ","), readyCount, true
3922}
3923
3924func CareState(tokenID string) (belly, cheer, acorns, totalXp, lastPatAtUnix, lastHappyEventAtUnix, lastActionAtUnix int64, found bool) {
3925	g, ok := gnomeByTokenID(tokenID)
3926	if !ok {
3927		return 0, 0, 0, 0, 0, 0, 0, false
3928	}
3929	owner, hasOwner := ownerOfToken(tokenID)
3930	acornBalance := int64(0)
3931	if hasOwner {
3932		acornBalance = balanceOf(owner)
3933	}
3934	now := time.Now().Unix()
3935	return meterNow(g.belly, g.bellyAt, now, bellyDropEveryHours),
3936		meterNow(g.cheer, g.cheerAt, now, cheerDropEveryHours),
3937		acornBalance, g.totalXp, g.lastPatAt, g.lastHappyEventAt, g.lastActionAt, true
3938}
3939
3940func MeterState(tokenID string) (belly, bellyAtUnix, cheer, cheerAtUnix int64, found bool) {
3941	g, ok := gnomeByTokenID(tokenID)
3942	if !ok {
3943		return 0, 0, 0, 0, false
3944	}
3945	return g.belly, g.bellyAt, g.cheer, g.cheerAt, true
3946}
3947
3948func Progress(tokenID string) (xpGarden, xpTinker, harvestCount, lastTinkerAtUnix, caredDayCount, dailyCareXpSpent, dailyCareDay int64, found bool) {
3949	g, ok := gnomeByTokenID(tokenID)
3950	if !ok {
3951		return 0, 0, 0, 0, 0, 0, 0, false
3952	}
3953	return g.xpGarden, g.xpTinker, g.harvestCount, g.lastTinkerAt,
3954		g.caredDayCount, g.dailyCareXp, g.dailyCareDay, true
3955}
3956
3957func CaredDays(tokenID string) (daysCsv string, found bool) {
3958	g, ok := gnomeByTokenID(tokenID)
3959	if !ok {
3960		return "", false
3961	}
3962	return caredDaysCsv(g), true
3963}
3964
3965func Mood(tokenID string) (mood string, found bool) {
3966	g, ok := gnomeByTokenID(tokenID)
3967	if !ok {
3968		return "", false
3969	}
3970	now := time.Now().Unix()
3971	belly := meterNow(g.belly, g.bellyAt, now, bellyDropEveryHours)
3972	return moodNow(belly, g.lastPatAt, g.lastHappyEventAt, g.adoptedAt, now, hungryAtOrBelow), true
3973}
3974
3975// IsLocked reports whether tokenID is currently refused by List's lockCaredDays check -- true
3976// only for a named one-of-one with fewer than lockCaredDays cared days; false for every ordinary
3977// gnome, always, and false for a named one-of-one once it has enough cared days.
3978func IsLocked(tokenID string) (locked bool, found bool) {
3979	g, ok := gnomeByTokenID(tokenID)
3980	if !ok {
3981		return false, false
3982	}
3983	return g.character != "" && g.caredDayCount < lockCaredDays, true
3984}
3985
3986func CollectionInfo() (name, symbol string) {
3987	return collectionName, collectionSymbol
3988}
3989
3990func TotalSupply() int64 {
3991	return token.TotalSupply()
3992}
3993
3994func BalanceOf(addr address) int64 {
3995	n, err := token.BalanceOf(addr)
3996	if err != nil {
3997		return 0
3998	}
3999	return n
4000}
4001
4002func OwnerOfToken(tokenID string) (owner address, found bool) {
4003	return ownerOfToken(tokenID)
4004}
4005
4006func TotalGnomes() int64 {
4007	return token.TotalSupply()
4008}
4009
4010func Acorns(addr address) (acorns int64, found bool) {
4011	if !balances.Has(addr.String()) {
4012		return 0, false
4013	}
4014	return balanceOf(addr), true
4015}
4016
4017// ---- counted rarity readers (MINT.md section 13): "nine gnomes in the world have this nose"
4018// has to be a chain fact. These four, plus CharacterOwner below, are that fact.
4019
4020func EyesCount(id int64) int64    { return eyesCount(id) }
4021func NoseCount(id int64) int64    { return noseCount(id) }
4022func HatCount(hat string) int64   { return hatCount(hat) }
4023func CoatCount(coat string) int64 { return coatCount(coat) }
4024
4025// CharacterOwner resolves one of the eight named one-of-ones, or the genesis gnome under its
4026// own name "sprig", to its tokenID, once minted. (Second-audit-pass nitpick: an earlier
4027// version of this comment said only "one of the three named one-of-ones", which stopped being
4028// true the moment init() registered "sprig" here too -- see init()'s own comment for why that
4029// registration is safe and never touches the editions' cap. The count in it then went stale a
4030// second way when v7 change 10 took the roster to eight; both are fixed here.)
4031func CharacterOwner(name string) (tokenID string, found bool) {
4032	v := characterOwner.Get(name)
4033	s, ok := v.(string)
4034	if !ok {
4035		return "", false
4036	}
4037	return s, true
4038}
4039
4040// ---- the mint's own state, read back.
4041
4042func IsAllowlisted(addr address) bool {
4043	return allowlist.Has(addr.String())
4044}
4045
4046func HasRolled(addr address) bool {
4047	return hasRolled.Has(addr.String())
4048}
4049
4050func PendingClaim(addr address) (round, claimedAtUnix int64, found bool) {
4051	v := pendingClaims.Get(addr.String())
4052	pc, ok := v.(*pendingClaim)
4053	if !ok || pc == nil {
4054		return 0, 0, false
4055	}
4056	return pc.round, pc.claimedAt, true
4057}
4058
4059// BatchStatus: mintedCount is reservations made by Claim (gates batchCap); settledCount is
4060// completions made by Settle (always <= mintedCount, the gap being pending, unsettled claims);
4061// namedMinted is how many of the eight named one-of-ones have actually been minted so far --
4062// read live off characterOwner rather than a separate counter, so there is only ever one source
4063// of truth for "has this edition been taken".
4064func BatchStatus() (mintedSoFar, settledSoFar, batchCapOut, namedMinted, namedCapOut int64) {
4065	// NAMED RETURNS DELIBERATELY DO NOT SHARE A NAME WITH THE PACKAGE-LEVEL mintedCount/
4066	// settledCount VARS. An earlier version of this function used mintedCount/settledCount as
4067	// its own named returns, which SHADOWED the package vars for the whole function body, so
4068	// `return mintedCount, settledCount, ...` returned the zero-valued local named returns
4069	// instead of the real package state -- this function always reported a fresh batch,
4070	// silently, with no error, which TestBatchClosesAtCapAndRefusesFurtherClaims caught by
4071	// actually driving claims until the realm said the batch was full and finding it never did.
4072	// Found and fixed before audit; recorded here rather than silently corrected, the same
4073	// discipline this file's own package doc uses for the Settle timing flaw.
4074	nm := int64(0)
4075	for _, name := range namedEditionOrder {
4076		if characterOwner.Has(name) {
4077			nm++
4078		}
4079	}
4080	return mintedCount, settledCount, batchCap, nm, numNamedEditions
4081}
4082
4083// ============================================================================
4084// v7: THE TRAIT READERS, AND THE SHAPE RULE THEY ALL OBEY.
4085// ============================================================================
4086//
4087// GAMES.md:38 is unambiguous -- "ship enumerators on day one; they cannot be added later
4088// (arena/v1 lacks them, forever)" -- so this list is complete and this is the only chance to get
4089// it right. Granular, one axis-group per call, matching GnomeByToken's style: explicit typed
4090// returns, no parsing, and `found` reporting whether the TOKEN exists and never whether a field
4091// is interesting. No v6 tuple is widened: "the app already reads v5's shapes, and widening a
4092// tuple return breaks every existing caller for no gain that a dedicated reader does not already
4093// give. Reads are free and unmetered, so 'one more query' costs a caller nothing; a changed
4094// signature costs them a rewrite."
4095//
4096// THE SHAPE RULE, STATED ONCE SO IT CAN BE CHECKED. Enumerate-and-read is permitted; QUERY IS
4097// NOT. Every reader in v7 is one of exactly four shapes:
4098//
4099//	1. X(tokenID)                      -- the stored facts about one token the caller named
4100//	2. TotalX() / XPage(offset, limit) -- a count and a window over a collection, in its own order
4101//	3. X(someKey)                      -- one index lookup on a key that is NOT a trait: an
4102//	                                      address, a name, a batch index, a part id
4103//	4. X()                             -- a realm constant
4104//
4105// THERE IS NO FIFTH SHAPE. In particular: no reader that takes a trait, a stat, a secret, a kin
4106// type or a predicate and returns the gnomes matching it, and NO READER THAT TAKES TWO TOKEN
4107// IDS. That is what makes TraitCensus, rarityScore, LineCount, IsKin, KinOf, "find me every
4108// gnome with fear 3" and every kin-bonus/related-pair/shared-quest feature STRUCTURALLY ABSENT
4109// rather than merely omitted -- they all need a shape this realm does not have.
4110//
4111// DELIBERATELY ABSENT, and this is the list to check a future version against: CareThreshold,
4112// CareScore, Band, PointsToThreshold and anything returning a fraction or remainder of any of
4113// them (TRAITS.md section 5.1's whole design is that there is nothing to put a countdown on);
4114// SecretCount, SecretCensus, HoldersOfSecret, IsFirstSeventeen and anything exposing
4115// secretsDrawnMask (an owner who can see which secrets are taken deduces others' by
4116// elimination); Label and Archetype (TRAITS.md section 2.3: the label is derived off chain,
4117// never stored, and a realm function returning it would make the two label-word columns
4118// unrewritable forever); TraitsMatching and every filter or predicate; any unique-in-batch or
4119// scarcest-in-batch marker (TRAITS.md section 2.3 recommends against building it at all at
4120// n = 60, where it fires about once per four hundred cards, and notes the honest alternative is
4121// "a rarity count wearing a hat"); KinOf, IsKin, LineOf, LineCount, KinCount and anything
4122// returning a degree; and any name, label or definition for any trait id.
4123
4124// Traits returns the five rolled axis ids, in AXIS order (0 temperament, 1 obsession, 2 bond,
4125// 3 fear, 4 boast) and NOT in dominance order. All five are 0 for the genesis gnome, which is
4126// the one token never produced by a roll.
4127func Traits(tokenID string) (temperament, obsession, bond, fear, boast int64, found bool) {
4128	g, ok := gnomeByTokenID(tokenID)
4129	if !ok {
4130		return 0, 0, 0, 0, 0, false
4131	}
4132	return g.temperament, g.obsession, g.bond, g.fear, g.boast, true
4133}
4134
4135// Dominance returns the five axis indexes 0..4 in DOMINANCE order: `first` is the loudest.
4136// Where the loud/latent boundary falls is an OFF-CHAIN reading of this number and is not stored
4137// (TRAITS.md sections 2.1 and 2.2) -- which is exactly what keeps two-states-versus-three
4138// reversible after deploy, in either direction, for free.
4139func Dominance(tokenID string) (first, second, third, fourth, fifth int64, found bool) {
4140	g, ok := gnomeByTokenID(tokenID)
4141	if !ok {
4142		return 0, 0, 0, 0, 0, false
4143	}
4144	return g.dominance[0], g.dominance[1], g.dominance[2], g.dominance[3], g.dominance[4], true
4145}
4146
4147// Stats returns the four Top Trumps stats. They sum to 22, one is >= 8 and one is <= 3.
4148// NOTHING IN ANY REALM COMPARES TWO GNOMES' STATS: there is no Top Trumps game on chain, no
4149// reader here takes two token ids, and TRAITS.md section 4.5's tie rule is social.
4150func Stats(tokenID string) (pluck, nous, grumble, appetite int64, found bool) {
4151	g, ok := gnomeByTokenID(tokenID)
4152	if !ok {
4153		return 0, 0, 0, 0, false
4154	}
4155	return g.pluck, g.nous, g.grumble, g.appetite, true
4156}
4157
4158// Secret returns the gnome's secret id, 0..16. THE NAME AND THE LINE LIVE OFF CHAIN
4159// (TRAITS.md section 3.2); this realm stores and returns the id and nothing else. There is no
4160// count of who holds what and no way to ask which secrets are still unheld.
4161func Secret(tokenID string) (secretID int64, found bool) {
4162	g, ok := gnomeByTokenID(tokenID)
4163	if !ok {
4164		return 0, false
4165	}
4166	return g.secret, true
4167}
4168
4169// BornUnder returns the card-only sign id, 0..7. It is not an axis, has no slot in the dominance
4170// order, and is never handed to the dialogue layer (TRAITS.md section 1.6) -- the realm
4171// guarantees the middle clause structurally and the last one is an off-chain discipline.
4172func BornUnder(tokenID string) (sign int64, found bool) {
4173	g, ok := gnomeByTokenID(tokenID)
4174	if !ok {
4175		return 0, false
4176	}
4177	return g.bornUnder, true
4178}
4179
4180// BatchIndex returns this gnome's 1..60 position in Batch One, which is the key LINEAGE.md
4181// section 2.4's kinship table is written against. 0 for the genesis gnome, which is outside the
4182// batch and is the only token for which this is ever 0.
4183func BatchIndex(tokenID string) (index int64, found bool) {
4184	g, ok := gnomeByTokenID(tokenID)
4185	if !ok {
4186		return 0, false
4187	}
4188	return g.batchIndex, true
4189}
4190
4191// TokenByBatchIndex is BatchIndex inverted: the token carrying a batch position, or found=false
4192// if that position has not been settled yet. Both directions ship because a client rendering a
4193// holder's cousins has batch indexes from the kinship table and needs token ids, and a client
4194// rendering one gnome's card has a token id and needs its index.
4195func TokenByBatchIndex(index int64) (tokenID string, found bool) {
4196	tid, ok := batchIndexToToken.Get(strconv.FormatInt(index, 10)).(string)
4197	if !ok || tid == "" {
4198		return "", false
4199	}
4200	return tid, true
4201}
4202
4203// TraitsCsv returns all seventeen stored trait numbers in one query, in a fixed, test-pinned
4204// order, for a client rendering a card -- the same CSV shape GardenState and CaredDays already
4205// use. It exists because TRAITS.md section 6.2's card renders these fields at once and six round
4206// trips to draw one card is silly when one would do.
4207//
4208// It is a CONVENIENCE OVER THE SAME STORED VALUES, not a second source of truth, and
4209// TestTraitsCsvAgreesWithEveryGranularReader pins that -- the same discipline NameAvailability
4210// already follows against SetName.
4211//
4212// THE CARE THRESHOLD IS NOT IN IT and has no reader anywhere. Neither is anything derived from
4213// it, and neither is a name, a label or an archetype.
4214//
4215//	temperament,obsession,bond,fear,boast,
4216//	dom1,dom2,dom3,dom4,dom5,
4217//	secret,pluck,nous,grumble,appetite,bornUnder,batchIndex
4218func TraitsCsv(tokenID string) (csv string, found bool) {
4219	g, ok := gnomeByTokenID(tokenID)
4220	if !ok {
4221		return "", false
4222	}
4223	fields := []int64{
4224		g.temperament, g.obsession, g.bond, g.fear, g.boast,
4225		g.dominance[0], g.dominance[1], g.dominance[2], g.dominance[3], g.dominance[4],
4226		g.secret, g.pluck, g.nous, g.grumble, g.appetite, g.bornUnder, g.batchIndex,
4227	}
4228	parts := make([]string, 0, len(fields))
4229	for _, v := range fields {
4230		parts = append(parts, strconv.FormatInt(v, 10))
4231	}
4232	return strings.Join(parts, ","), true
4233}
4234
4235// ============================================================================
4236// v7: THE KINSHIP ENUMERATORS, AND THEY ARE THE ONLY WAY IN.
4237// ============================================================================
4238//
4239// LINEAGE.md section 6.2 argues that the day-one enumerator rule is a TRAP here: "Store the web
4240// and you must ship KinOf(tokenID) on day one; ship KinOf and it exists forever; and the first
4241// third-party UI to find it will build LineCount() on top of it within a week." The OVERRULED
4242// box moved the position on storing but left section 6.5's prohibitions standing, so the design
4243// has to satisfy both -- and it does, by a distinction that is not a dodge:
4244//
4245//   - KinOf(tokenID) is an ANSWER ABOUT A GNOME. KinEdgesPage(offset, limit) is THE TABLE. The
4246//     first is the shape a contract calls. The second is the shape a RENDERER calls: it takes no
4247//     token, no address and no pair, and it cannot be given one. A client that wants a holder's
4248//     cousins reads 84 rows once, caches them, and filters locally -- precisely what it already
4249//     does with the published Markdown today, except now the rows are owned, dated by a block
4250//     height, and readable by any realm that wants the DATA rather than an answer.
4251//   - NOTHING IS FORECLOSED THAT WAS NOT ALREADY OPEN. A third party can of course build KinOf
4252//     on top of 84 rows; they could build it on top of the published document too. Section 6.2's
4253//     concern was never that the data is derivable, it was that A REALM FUNCTION would exist for
4254//     a contract to call and a UI to find without looking.
4255//   - THE LOAD-BEARING LINE STILL HOLDS EXACTLY: "no entrypoint anywhere in any realm that
4256//     behaves differently for two related gnomes than for two strangers." v7 has none, cannot
4257//     grow one, and these two functions are not one -- they do not take a gnome.
4258
4259// TotalKinEdges returns 84. The table is fixed at deploy and this number never changes.
4260func TotalKinEdges() int64 { return numKinEdges }
4261
4262// KinEdgesPage returns rows "from,to,typeId" -- batch indexes, not token ids; type ids, not
4263// words -- in the table's own order (LINEAGE.md section 2.4's own row order), offset/limit
4264// paginated exactly as TokensPage and ListingsPage already are, with the same refusals.
4265func KinEdgesPage(offset, limit int) []string {
4266	if offset < 0 {
4267		panic("garden: offset must not be negative")
4268	}
4269	if limit <= 0 {
4270		panic("garden: limit must be positive")
4271	}
4272	if limit > maxKinEdgesPageSize {
4273		limit = maxKinEdgesPageSize
4274	}
4275	out := make([]string, 0, limit)
4276	for i := offset; i < len(kinEdges) && len(out) < limit; i++ {
4277		e := kinEdges[i]
4278		out = append(out, strconv.FormatInt(e.from, 10)+","+
4279			strconv.FormatInt(e.to, 10)+","+
4280			strconv.FormatInt(e.typ, 10))
4281	}
4282	return out
4283}
4284
4285// TotalKinTypes returns 9, LINEAGE.md section 2.4's closed vocabulary.
4286func TotalKinTypes() int64 { return numKinTypes }
4287
4288// KinTypeName maps a type id to its one word. The declared exception to "wording off chain" --
4289// see kinTypeNames' own comment for the three-part argument and for how to remove it in one
4290// edit if the owner prefers the strictest reading.
4291func KinTypeName(typeID int64) (name string, found bool) {
4292	if typeID < 0 || typeID >= numKinTypes {
4293		return "", false
4294	}
4295	return kinTypeNames[typeID], true
4296}
4297
4298func GenesisTokenID() string {
4299	return genesisTokenID
4300}
4301
4302func AdminState() (admin address, frozen bool) {
4303	return address(adminAddress), rulesFrozen
4304}
4305
4306func LastWakeDay(addr address) (day int64, found bool) {
4307	v, ok := lastWakeDay.Get(addr.String()).(int64)
4308	return v, ok
4309}
4310
4311// ============================================================================
4312// Render
4313// ============================================================================
4314
4315// Render DELIBERATELY ECHOES NO PLAYER-SUPPLIED STRING, and v6 keeps it that way even though it
4316// now has gnome names it could print. THIS IS THE DEFENCE AGAINST ABUSIVE NAMES ON A PUBLIC PAGE,
4317// and it is structural rather than a filter: gnoweb renders this output as markdown, so a page
4318// that listed names would be a markdown sink fed by arbitrary player input, and the project would
4319// then own a moderation problem on an immutable realm with no moderation surface. v5's own
4320// adversarial review (ATTACK-REVIEW.md, "Render sanitisation") recorded "no injection surface" as
4321// a property of this function; adding a name here would have been the one change in this file
4322// that falsified an audited claim.
4323//
4324// Belt and braces, because "nothing echoes names today" is only one edit away from being false:
4325// the name charset allowlist (letters, digits, space, hyphen, apostrophe -- see validNameRune)
4326// excludes every markdown metacharacter there is. No '[', ']', '(', ')', '<', '>', '*', '_',
4327// backtick, pipe or newline can appear in a name, so even a future Render that DID print one
4328// could not be made to inject link syntax, a raw HTML tag or a table break. An abusive name is
4329// therefore limited to being rude in words, visible only to someone who queries that specific
4330// token, never on a page this project publishes.
4331// v7 EXTENDS Render WITH ONE PATH -- a token id -- AND THREE CONSTRAINTS, all of which are the
4332// forbidden list restated for the one function that produces human-facing output:
4333//
4334//   - NO THRESHOLD, NO SCORE, NO BAND, NO FRACTION, NO REMAINDER. TRAITS.md section 6.2: "Never
4335//     on the card, in any version: ... the care score, the care threshold, or anything derived
4336//     from either."
4337//   - NO CENSUS, NO DISTRIBUTION, NO COUNT OF ANYTHING TRAIT-SHAPED, and specifically no
4338//     unique-in-batch or scarcest-in-batch marker.
4339//   - IDS, NOT NAMES. This function prints the numbers it stores. It does not get a copy of the
4340//     thirty-two public names, the seventeen secret names or the eight signs, because that would
4341//     put revisable wording on an immutable realm -- the whole point of the split. Rendering
4342//     "Thistle" instead of 7 is the app's job, from TRAITS.md's own tables.
4343//
4344// AND IT STILL ECHOES NO PLAYER-SUPPLIED STRING: an unknown path returns a fixed message with
4345// the path omitted, exactly as v6's did, so the markdown sink stays unreachable from player
4346// input even though this function now takes a path that resolves.
4347func Render(path string) string {
4348	if path != "" {
4349		// A token id, or nothing. The path is never echoed back -- see the comment above.
4350		if csv, ok := TraitsCsv(path); ok {
4351			g, _ := gnomeByTokenID(path)
4352			out := "# Gnotchi gnome\n\n"
4353			out += "Batch One position: " + strconv.FormatInt(g.batchIndex, 10) + " of " + strconv.FormatInt(batchCap, 10) + "\n\n"
4354			out += "Settled at drand round: " + strconv.FormatInt(g.settledRound, 10) + "\n\n"
4355			out += "Axes (temperament, obsession, bond, fear, boast), as ids: " +
4356				strconv.FormatInt(g.temperament, 10) + ", " + strconv.FormatInt(g.obsession, 10) + ", " +
4357				strconv.FormatInt(g.bond, 10) + ", " + strconv.FormatInt(g.fear, 10) + ", " +
4358				strconv.FormatInt(g.boast, 10) + "\n\n"
4359			out += "Dominance order (axis indexes, loudest first): " +
4360				strconv.FormatInt(g.dominance[0], 10) + ", " + strconv.FormatInt(g.dominance[1], 10) + ", " +
4361				strconv.FormatInt(g.dominance[2], 10) + ", " + strconv.FormatInt(g.dominance[3], 10) + ", " +
4362				strconv.FormatInt(g.dominance[4], 10) + "\n\n"
4363			out += "Stats (pluck, nous, grumble, appetite): " +
4364				strconv.FormatInt(g.pluck, 10) + ", " + strconv.FormatInt(g.nous, 10) + ", " +
4365				strconv.FormatInt(g.grumble, 10) + ", " + strconv.FormatInt(g.appetite, 10) + "\n\n"
4366			out += "Secret id: " + strconv.FormatInt(g.secret, 10) + "\n\n"
4367			out += "Born under (sign id): " + strconv.FormatInt(g.bornUnder, 10) + "\n\n"
4368			out += "All of the above as one CSV: " + csv + "\n\n"
4369			out += "The names for these ids live in TRAITS.md, off chain and still revisable. " +
4370				"This realm stores and prints the numbers.\n"
4371			return out
4372		}
4373		return "garden/v7: unknown path"
4374	}
4375	minted, settled, cap_, namedMinted, namedCap := BatchStatus()
4376	out := "# Gnotchi gnomes -- garden/v7\n\n"
4377	out += "Batch One: " + strconv.FormatInt(minted, 10) + " claimed, " +
4378		strconv.FormatInt(settled, 10) + " settled, of " + strconv.FormatInt(cap_, 10) + " ever.\n\n"
4379	out += "Named one-of-ones minted: " + strconv.FormatInt(namedMinted, 10) + " of " + strconv.FormatInt(namedCap, 10) + ".\n\n"
4380	out += "Total supply (including the genesis gnome): " + strconv.FormatInt(TotalSupply(), 10) + "\n\n"
4381	out += "Free to claim, if your address is on the allowlist. One roll per wallet, ever. " +
4382		"Claim pins a future drand round; Settle, callable by anyone once that round is " +
4383		"published, mints the result.\n"
4384	return out
4385}
4386
4387// ============================================================================
4388// Internal helpers
4389// ============================================================================
4390
4391func gnomeByTokenID(tokenID string) (*gnome, bool) {
4392	v := gnomes.Get(tokenID)
4393	g, ok := v.(*gnome)
4394	if !ok || g == nil {
4395		return nil, false
4396	}
4397	return g, true
4398}
4399
4400// activeGnomeForAddr resolves addr's AWAKE gnome, if any. This is the one and only lookup
4401// SaveDay and ChooseType use -- there is deliberately no "the gnome for this address" any more.
4402func activeGnomeForAddr(addr address) (tid string, g *gnome, found bool) {
4403	tidStr, ok := activeToken.Get(addr.String()).(string)
4404	if !ok || tidStr == "" {
4405		return "", nil, false
4406	}
4407	gg, ok := gnomeByTokenID(tidStr)
4408	if !ok {
4409		return "", nil, false
4410	}
4411	return tidStr, gg, true
4412}
4413
4414func ownerOfToken(tokenID string) (owner address, found bool) {
4415	if !gnomes.Has(tokenID) {
4416		return address(""), false
4417	}
4418	o, err := token.OwnerOf(grc721.TokenID(tokenID))
4419	if err != nil {
4420		return address(""), false
4421	}
4422	return o, true
4423}
4424
4425// addOwnerToken / removeOwnerToken: the hold-many index. ownerTokens stores a *[]string per
4426// owner so repeated appends mutate one allocation rather than replacing the tree entry every
4427// time; removal does a linear scan, which is fine at this realm's permanent, tiny upper bound
4428// (batchCap = 60 tokens that will ever exist, minus the one permanently unreachable genesis
4429// gnome, so no owner's slice is ever longer than 60 entries for the whole life of this realm --
4430// v5 and v6 argued the identical point at 24; only the number moved).
4431func addOwnerToken(addr address, tokenID string) {
4432	v := ownerTokens.Get(addr.String())
4433	list, ok := v.(*[]string)
4434	if !ok || list == nil {
4435		fresh := []string{tokenID}
4436		ownerTokens.Set(addr.String(), &fresh)
4437		return
4438	}
4439	*list = append(*list, tokenID)
4440}
4441
4442func removeOwnerToken(addr address, tokenID string) {
4443	v := ownerTokens.Get(addr.String())
4444	list, ok := v.(*[]string)
4445	if !ok || list == nil {
4446		return
4447	}
4448	out := make([]string, 0, len(*list))
4449	for _, id := range *list {
4450		if id != tokenID {
4451			out = append(out, id)
4452		}
4453	}
4454	*list = out
4455}
4456
4457func balanceOf(addr address) int64 {
4458	v := balances.Get(addr.String())
4459	n, ok := v.(int64)
4460	if !ok {
4461		return 0
4462	}
4463	return n
4464}
4465
4466func setBalance(addr address, v int64) {
4467	if v < 0 {
4468		panic("garden: acorn balance cannot go negative")
4469	}
4470	balances.Set(addr.String(), v)
4471}
4472
4473func grantStartingPurseIfNew(addr address) {
4474	if !balances.Has(addr.String()) {
4475		balances.Set(addr.String(), acornsStart)
4476	}
4477}
4478
4479func feeFor(priceUgnot int64) int64 {
4480	return fee.floorUgnot + priceUgnot*fee.rateBps/10000
4481}
4482
4483func exactUgnot(send chain.Coins, want int64) bool {
4484	if len(send) != 1 {
4485		return false
4486	}
4487	return send[0].Denom == ugnotDenom && send[0].Amount == want
4488}
4489
4490func coinsString(c chain.Coins) string {
4491	if len(c) == 0 {
4492		return "nothing"
4493	}
4494	out := ""
4495	for i, coin := range c {
4496		if i > 0 {
4497			out += ","
4498		}
4499		out += strconv.FormatInt(coin.Amount, 10) + coin.Denom
4500	}
4501	return out
4502}
4503
4504func listingFor(tokenID string) (*listing, bool) {
4505	v := listings.Get(tokenID)
4506	l, ok := v.(*listing)
4507	if !ok || l == nil {
4508		return nil, false
4509	}
4510	if l.priceUgnot < 0 {
4511		return nil, false
4512	}
4513	return l, true
4514}
4515
4516// ============================================================================
4517// v6 FIX 1: name validation and canonicalisation. validNameLength/validNameCharset/
4518// hasVisibleNameContent/validNameRune/invisibleLetterRunes are v4's OWN IMPLEMENTATIONS, carried
4519// back byte-for-byte -- three audit rounds' worth of work (v4's own comment), including v2's
4520// audit fix F3 (\uXXXX escapes rather than literal invisible glyphs, so the denylist is readable
4521// in an editor and cannot be silently emptied by a formatter that strips default-ignorable
4522// characters). They are NOT re-derived here and must not be "tidied": every line of that
4523// allowlist was argued for once already.
4524//
4525// THE DENYLIST IS THE PART THAT MATTERS AND THE PART THAT LOOKS REDUNDANT. All four entries are
4526// genuinely LETTERS by Unicode's own classification, so unicode.IsLetter accepts every one of
4527// them -- and all four render as NOTHING AT ALL. They exist in fonts purely as filler. Without
4528// this list, "Hob" and "Hob<U+3164>" are different names that look identical in every UI either
4529// will ever be shown in, which is impersonation with no defence available to the person being
4530// impersonated. app/js/config.js's NAME_INVISIBLE_LETTERS is the same four code points, exposed
4531// there for the app's own tests; TestEveryInvisibleLetterIsRefused table-drives this map directly
4532// so neither side's coverage can silently fall behind the other (config.js's finding Y3: before
4533// that change every test on both sides of the stack only ever exercised U+3164).
4534//
4535// CARRIED FORWARD UNCHANGED, AND STILL AN OPEN PRODUCT QUESTION, NOT A SETTLED ONE: this
4536// allowlist excludes every COMBINING MARK (chiefly Unicode category Mn), which makes zalgo names
4537// structurally impossible -- and also makes NFD Latin, Devanagari, Thai, vocalised Hebrew and
4538// Arabic unspellable. realms/garden/NOTES.md records that trade-off in full as the owner's to
4539// revisit (v4's third audit round, finding Y7). v6 carries it forward rather than silently
4540// re-deciding it, exactly as v4 carried it forward from v1/v2. Precomposed letters (é as a single
4541// code point) are accepted fine; the exclusion is specifically and only about combining marks.
4542// ============================================================================
4543
4544func validNameLength(name string) bool {
4545	// RuneCountInString, never len(): len() counts BYTES, so a 24-character name in any
4546	// non-ASCII script would be refused for being "too long" while an ASCII one of the same
4547	// visible length passed.
4548	n := utf8.RuneCountInString(name)
4549	return n > 0 && n <= maxNameLen
4550}
4551
4552func validNameCharset(name string) bool {
4553	for _, r := range name {
4554		if !validNameRune(r) {
4555			return false
4556		}
4557	}
4558	return true
4559}
4560
4561func hasVisibleNameContent(name string) bool {
4562	for _, r := range name {
4563		if r != ' ' {
4564			return true
4565		}
4566	}
4567	return false
4568}
4569
4570// invisibleLetterRunes: the four code points that are Unicode LETTERS and render as nothing. See
4571// the section comment above for why a letter allowlist alone cannot catch these. Written as
4572// \uXXXX escapes (v2's audit fix F3) so they survive an editor or formatter that strips
4573// default-ignorable characters -- a denylist that can be silently emptied protects nothing.
4574//
4575// v3 through v5 carried this comment while the entries themselves were literal invisible glyphs,
4576// so the comment was false about its own code for three versions and the protection it describes
4577// was not actually in place. Restored here, with the escapes BACK IN THE SOURCE this time.
4578//
4579// The escapes alone are only half of F3. TestEveryInvisibleLetterIsRefused table-drives this map,
4580// which means it tests whatever the map happens to contain -- swap one entry for an ordinary
4581// letter and that test still passes while the realm ships banning the letter and accepting the
4582// invisible one. TestInvisibleLetterRunesIsExactlyFourPinnedCodepoints is the other half: it
4583// pins all four code points BY ESCAPE, independently of this map's contents, so that
4584// substitution fails loudly.
4585var invisibleLetterRunes = map[rune]bool{
4586	'\u115f': true, // HANGUL CHOSEONG FILLER
4587	'\u1160': true, // HANGUL JUNGSEONG FILLER
4588	'\u3164': true, // HANGUL FILLER
4589	'\uffa0': true, // HALFWIDTH HANGUL FILLER
4590}
4591
4592func validNameRune(r rune) bool {
4593	switch r {
4594	case ' ', '-', '\'':
4595		return true
4596	}
4597	if invisibleLetterRunes[r] {
4598		return false
4599	}
4600	return unicode.IsLetter(r) || unicode.IsDigit(r)
4601}
4602
4603// smartPunctuationRunes: the four code points iOS substitutes by default for a straight
4604// apostrophe or hyphen. NONE of them is accepted by validNameRune -- this map exists only so the
4605// refusal can SAY SO instead of reporting a generic charset failure to a player who typed an
4606// ordinary apostrophe on an ordinary phone. See nameAssignabilityProblem's own comment on why
4607// this realm refuses rather than folds them.
4608var smartPunctuationRunes = map[rune]bool{
4609	'‘': true, // LEFT SINGLE QUOTATION MARK
4610	'’': true, // RIGHT SINGLE QUOTATION MARK
4611	'–': true, // EN DASH
4612	'—': true, // EM DASH
4613}
4614
4615func hasSmartPunctuation(name string) bool {
4616	for _, r := range name {
4617		if smartPunctuationRunes[r] {
4618			return true
4619		}
4620	}
4621	return false
4622}
4623
4624// canonicalNameKey folds a name to the key nameOwner and reservedNameKeys compare on: lowercase
4625// ASCII, runs of spaces collapsed to one, leading and trailing spaces removed.
4626//
4627// THE STORED NAME AND THE KEY ARE DELIBERATELY TWO DIFFERENT STRINGS. The gnome keeps the name
4628// its owner typed, exactly as typed, because that is what a person chose and the app should show
4629// it back to them unaltered. The KEY exists only to answer "is this the same name as that one"
4630// for uniqueness and reservation, and for THAT question "Hob", "hob", "HOB" and "Hob" with two
4631// spaces in it are all plainly the same name. Keying on the raw string instead would make
4632// uniqueness worthless and would let anyone walk around the one-of-one reservation by changing
4633// one letter's case.
4634//
4635// WHAT IT DELIBERATELY DOES NOT DO, stated plainly rather than left for a reader to discover:
4636// it does not close cross-script HOMOGLYPHS. Cyrillic "А", Greek "Ο" and fullwidth "A" are all
4637// Unicode letters, all accepted, and all visually confusable with their Latin counterparts, so
4638// "Нob" with a Cyrillic Н is a different key from "Hob" and will be allowed. This is the SAME
4639// accepted residual app/js/config.js's NAME_CHARSET_RE documents in its own comment, carried
4640// forward knowingly: closing it properly needs a confusables table (thousands of mappings,
4641// versioned by Unicode, compiled into an immutable realm) and the honest cost/benefit at a
4642// 60-gnome collection does not justify it. The defence that IS in place for the case that
4643// actually matters -- the eight cap-one editions -- is that their names are reserved and
4644// unrenameable, so a homoglyph impostor can exist but can never hold the real key, and
4645// CharacterOwner (not a name lookup) is what resolves the genuine Hob.
4646//
4647// ASCII-only lowercasing, following lowerASCII's own v4 reasoning: strings.ToLower would apply
4648// locale-independent Unicode case folding whose behaviour is a toolchain-version detail on an
4649// immutable realm, and the keys that MUST match exactly (reservedNameKeys) are all ASCII.
4650func canonicalNameKey(name string) string {
4651	lowered := lowerASCII(name)
4652
4653	out := make([]byte, 0, len(lowered))
4654	prevSpace := true // true at the start, so leading spaces are dropped by the same test
4655	for i := 0; i < len(lowered); i++ {
4656		c := lowered[i]
4657		if c == ' ' {
4658			if prevSpace {
4659				continue
4660			}
4661			prevSpace = true
4662			out = append(out, c)
4663			continue
4664		}
4665		prevSpace = false
4666		out = append(out, c)
4667	}
4668	// One trailing space may survive the loop above (a name ending in a space emits it before the
4669	// loop ends); drop it.
4670	if len(out) > 0 && out[len(out)-1] == ' ' {
4671		out = out[:len(out)-1]
4672	}
4673	return string(out)
4674}
4675
4676func nameOwnerOf(key string) (tokenID string, found bool) {
4677	v := nameOwner.Get(key)
4678	s, ok := v.(string)
4679	if !ok || s == "" {
4680		return "", false
4681	}
4682	return s, true
4683}
4684
4685// lowerASCII: v4's own helper, unchanged -- ASCII-only on purpose (see v4's own comment on why
4686// not strings.ToLower).
4687func lowerASCII(s string) string {
4688	b := []byte(s)
4689	for i, c := range b {
4690		if c >= 'A' && c <= 'Z' {
4691			b[i] = c + ('a' - 'A')
4692		}
4693	}
4694	return string(b)
4695}
4696
4697func anyPlotPlanted(g *gnome) bool {
4698	for i := 0; i < numPlots; i++ {
4699		if g.plots[i].plantedAt != 0 {
4700			return true
4701		}
4702	}
4703	return false
4704}
4705
4706func eyesCount(id int64) int64 {
4707	v := eyesCounts.Get(strconv.FormatInt(id, 10))
4708	n, ok := v.(int64)
4709	if !ok {
4710		return 0
4711	}
4712	return n
4713}
4714
4715func noseCount(id int64) int64 {
4716	v := noseCounts.Get(strconv.FormatInt(id, 10))
4717	n, ok := v.(int64)
4718	if !ok {
4719		return 0
4720	}
4721	return n
4722}
4723
4724func hatCount(hat string) int64 {
4725	v := hatCounts.Get(hat)
4726	n, ok := v.(int64)
4727	if !ok {
4728		return 0
4729	}
4730	return n
4731}
4732
4733func coatCount(coat string) int64 {
4734	v := coatCounts.Get(coat)
4735	n, ok := v.(int64)
4736	if !ok {
4737		return 0
4738	}
4739	return n
4740}
4741
4742// ---- parsing, meters, mood, level: v4's own proven implementations, byte-for-byte, with
4743// rule() lookups replaced by this file's fixed constants (see the package doc: no SetRule here).
4744
4745func parseActions(raw string) []saveAction {
4746	if raw == "" {
4747		return nil
4748	}
4749
4750	count := 1
4751	for i := 0; i < len(raw); i++ {
4752		if raw[i] == ';' {
4753			count++
4754		}
4755	}
4756	if count > maxActionsPerCall {
4757		panic("garden: too many actions in one call; the limit is " + strconv.Itoa(maxActionsPerCall))
4758	}
4759
4760	parts := strings.Split(raw, ";")
4761	actions := make([]saveAction, 0, len(parts))
4762	for _, part := range parts {
4763		if part == "" {
4764			panic("garden: empty action entry")
4765		}
4766		fields := strings.Split(part, ":")
4767		switch fields[0] {
4768		case actionPat, actionFeed, actionTinker:
4769			if len(fields) != 2 {
4770				panic("garden: malformed action: " + quoteAction(part))
4771			}
4772			actions = append(actions, saveAction{kind: fields[0], at: parseUnixField(fields[1], part)})
4773		case actionPlant, actionHarvest:
4774			if len(fields) != 3 {
4775				panic("garden: malformed action: " + quoteAction(part))
4776			}
4777			actions = append(actions, saveAction{
4778				kind: fields[0],
4779				plot: parsePlotField(fields[1], part),
4780				at:   parseUnixField(fields[2], part),
4781			})
4782		default:
4783			panic("garden: unknown action kind: " + quoteAction(part))
4784		}
4785	}
4786	return actions
4787}
4788
4789func quoteAction(part string) string {
4790	return "\"" + part + "\""
4791}
4792
4793func parseUnixField(s, part string) int64 {
4794	n, err := strconv.ParseInt(s, 10, 64)
4795	if err != nil || n < 0 {
4796		panic("garden: malformed timestamp in action: " + quoteAction(part))
4797	}
4798	return n
4799}
4800
4801func parsePlotField(s, part string) int {
4802	n, err := strconv.Atoi(s)
4803	if err != nil || n < 0 || n >= numPlots {
4804		panic("garden: malformed plot index in action: " + quoteAction(part))
4805	}
4806	return n
4807}
4808
4809func dayNumber(unixSeconds int64) int64 { return unixSeconds / daySeconds }
4810
4811// recordCaredDay reports, via banked, whether it actually incremented g.caredDayCount -- false
4812// on the no-op branch (day == g.lastCaredDay already, e.g. a second SaveDay call the same UTC
4813// day, or a token bought mid-day that the SELLER had already banked that exact day for before
4814// the sale). SaveDay's own caller-scoped gate (lastCaredDayForOwner) depends on this: it must
4815// only spend the owner's one-bank-per-day allowance on a call that actually banked something,
4816// never on a no-op -- a gno-auditor re-audit found that an unconditional advance let a buyer's
4817// SaveDay on a token the seller had already banked that day silently consume the buyer's own
4818// allowance for the rest of that day, denying an otherwise fully legitimate cared day on a
4819// DIFFERENT, genuinely active token later the same day. See the call site in SaveDay.
4820func recordCaredDay(g *gnome, day int64) (banked bool) {
4821	if g.lastCaredDay == 0 {
4822		g.lastCaredDay = day
4823		g.caredMask = 1
4824		g.caredDayCount = 1
4825		return true
4826	}
4827	if day == g.lastCaredDay {
4828		return false
4829	}
4830	shift := uint(day - g.lastCaredDay)
4831	if shift >= caredWindowDays {
4832		g.caredMask = 1
4833	} else {
4834		g.caredMask = (g.caredMask << shift) | 1
4835	}
4836	g.lastCaredDay = day
4837	g.caredDayCount++
4838	return true
4839}
4840
4841func caredDaysCsv(g *gnome) string {
4842	if g.lastCaredDay == 0 {
4843		return ""
4844	}
4845	days := make([]string, 0, caredWindowDays)
4846	for i := uint(0); i < caredWindowDays; i++ {
4847		if g.caredMask&(1<<i) != 0 {
4848			days = append(days, strconv.FormatInt(g.lastCaredDay-int64(i), 10))
4849		}
4850	}
4851	return strings.Join(days, ",")
4852}
4853
4854func meterNow(value, setAt, now, dropEveryHours int64) int64 {
4855	if dropEveryHours <= 0 {
4856		return value
4857	}
4858	elapsedHours := (now - setAt) / hourSeconds
4859	if elapsedHours <= 0 {
4860		return clampMeter(value)
4861	}
4862	drops := elapsedHours / dropEveryHours
4863	return clampMeter(value - drops)
4864}
4865
4866func clampMeter(v int64) int64 {
4867	if v < 0 {
4868		return 0
4869	}
4870	if v > meterMax {
4871		return meterMax
4872	}
4873	return v
4874}
4875
4876func canPat(lastPatAt, now, cooldownHours int64) bool {
4877	if lastPatAt == 0 {
4878		return true
4879	}
4880	return now-lastPatAt >= cooldownHours*hourSeconds
4881}
4882
4883func moodNow(belly, lastPatAt, lastHappyEventAt, adoptedAt, now, hungryThreshold int64) string {
4884	if belly <= hungryThreshold {
4885		return moodHungry
4886	}
4887	if lastHappyEventAt == 0 {
4888		if now-adoptedAt <= happyWindowHours*hourSeconds {
4889			return moodContent
4890		}
4891		return moodSulky
4892	}
4893	if now-lastHappyEventAt <= happyWindowHours*hourSeconds {
4894		return moodHappy
4895	}
4896	if now-lastHappyEventAt >= sulkyAfterHours*hourSeconds {
4897		return moodSulky
4898	}
4899	return moodContent
4900}
4901
4902func xpToNext(level int64) int64 { return levelBase + levelStep*level }
4903
4904func totalXpForLevel(level int64) int64 {
4905	if level <= 1 {
4906		return 0
4907	}
4908	total := int64(0)
4909	for l := int64(1); l < level; l++ {
4910		total += xpToNext(l)
4911	}
4912	return total
4913}
4914
4915func levelFromXp(totalXp int64) int64 {
4916	level := int64(1)
4917	for level < maxLevel {
4918		need := totalXpForLevel(level + 1)
4919		if totalXp < need {
4920			break
4921		}
4922		level++
4923	}
4924	return level
4925}