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

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