battlePuff
int64// battle.puff
Value
10
battleRounds
untyped bigint// battle.rounds
Value
(3 <untyped> bigint)
battleWinHit
int64// battle.winHit
Value
3
battleEdge
int64// battle.typeEdge
Value
1
battleTieLoss
int64// battle.tieLoss
Value
1
moveQuick
untyped stringValue
"Q"
moveStrong
untyped stringValue
"S"
moveGuard
untyped stringValue
"G"
typeGardener
untyped stringValue
"gardener"
typeExplorer
untyped stringValue
"explorer"
typeTinkerer
untyped stringValue
"tinkerer"
challengeExpiresHours
int64// time.challengeExpiresHours
Value
24
revealWindowHours
int64// time.revealWindowHours
Value
24
rankedFromLevel
int64// xp.rankedFromLevel
Value
3
rankedPerGnomePerDay
int64// ranked.perGnomePerDay
Value
3
rankedGamesPerPairPerWeek
int64// ranked.rankedGamesPerPairPerWeek
Value
2
hourSeconds
int64Value
3600
daySeconds
int64Value
86400
weekSeconds
int64Value
604800
stPending
untyped stringValue
"pending"
stAccepted
untyped stringValue
"accepted"
stResolved
untyped stringValue
"resolved"
stForfeited
untyped stringValue
"forfeited"
styleBeatsMap
map[string]stringstyleBeatsMap: Quick beats Strong, Strong beats Guard, Guard beats Quick (brief 3.6).
- OID
- 05791d…1039:4
styleBeatsMap details
typeBeatsMap
map[string]stringtypeBeatsMap: Gardener beats Explorer, Explorer beats Tinkerer, Tinkerer beats Gardener.
- OID
- 05791d…1039:6
typeBeatsMap details
arenaChallenge
typearenaChallenge is one sealed-plan battle, pending through to resolved/forfeited.
Value
v1.arenaChallenge
challenges
*v0.Tree// challengeID string -> \*arenaChallenge
- OID
- 05791d…1039:8
challenges details
nextID
v0.IDValue
<zero>
dailyRanked
*v0.Tree// "\<addr>:\<dayNumber>" -> int64 ranked-battle count
- OID
- 05791d…1039:12
dailyRanked details
pairWeekly
*v0.Tree// "\<addrLo>|\<addrHi>:\<weekNumber>" -> int64 ranked-result count
- OID
- 05791d…1039:15
pairWeekly details
totalChallenges
int64Value
6
totalResolved
int64Value
4
Challenge
func(opponent .uverse.address, commitHash string) stringChallenge posts only commitHash -- the sealed hash of the challenger's plan and salt. This realm never sees, and never could see, the plan itself at this stage: there is no plan parameter on this function at all, so a plan cannot arrive here even by mistake. See the package doc for why that asymmetry (challenger sealed, opponent's Accept plan in the clear) is deliberate and correct, not a gap. Challenge returns the new challenge's ID. This is an addition beyond the brief's own parameter-only signature notation, not a deviation from it: nothing in the brief forbids a return value, the caller genuinely needs the ID back to tell the opponent which challenge to Accept, and the alternative -- forcing every caller to parse the emitted "Challenge" event for the same value this function already has in hand -- is strictly worse for no safety benefit. See NOTES.md. THE unsafe.OriginSend() GUARD BELOW, AND ITS PRICE, recorded once here for all four mutating functions that use it. OriginSend reports the coins attached to the ORIGIN TRANSACTION, not the coins this call received -- which is exactly why the stdlib quarantines it under \`unsafe\`. Its purpose here is narrow and worth keeping: this realm has no banker, so any coin that reaches its address is stuck forever, and refusing the call is kinder than silently swallowing a user's GNOT. The price is permanent and goes in the other direction: because the check is transaction-scoped, any future transaction that attaches coins to some OTHER realm in the same call chain makes all four of these functions panic even though arena received nothing. A paid-entry tournament realm wrapping Challenge/Accept/Reveal is therefore impossible against this version. The read surface is unaffected -- ChallengeStatus, ChallengeProof and Render are non-crossing and carry no such check -- so a read-only season/v1 is fine; it is write-path composition specifically that is foreclosed. This is a forced trade, not an oversight: gno 0.9 exposes no call-scoped "coins sent to me" primitive at all. \`gno doc chain/runtime/unsafe\` lists OriginSend as the only coin receipt reader, and \`gno doc chain\` has no equivalent -- checked, not assumed. The stdlib's own advice is to pair it with runtime.AssertOriginCall(), which would forbid realm callers outright and so is strictly worse for composability than what is here.
- OID
- 05791d…1039:19
Challenge details
Accept
func(challengeID string, plan string)Accept posts the opponent's own plan in the clear -- they learn nothing from the sealed commit, so there is nothing to protect by hiding their own choice. See the package doc.
- OID
- 05791d…1039:21
Accept details
Reveal
func(challengeID string, plan string, salt string)Reveal posts the challenger's plan and salt. The realm recomputes commitDigest and rejects any plan/salt pair that does not reproduce the posted commitHash, then resolves the battle itself -- against both gnomes' CURRENT type and level, read live from garden/v2, never from anything cached here.
- OID
- 05791d…1039:22
Reveal details
ClaimForfeit
func(challengeID string)ClaimForfeit lets the opponent claim the win once revealWindowHours has passed with no Reveal. Per the brief: "the result counts as a normal ranked loss" for the challenger -- this realm applies the SAME ranked-eligibility rule to a forfeit as to a genuine reveal (see NOTES.md), rather than treating a forfeit as automatically ranked regardless of the fairness caps.
- OID
- 05791d…1039:23
ClaimForfeit details
ChallengeStatus
func(challengeID string) (challenger .uverse.address, opponent .uverse.address, status string, createdAt int64, acceptedAt int64, resolvedAt int64, winner string, puffChallenger int64, puffOpponent int64, ranked bool, found bool)- OID
- 05791d…1039:24
ChallengeStatus details
ChallengeProof
func(challengeID string) (commit string, challengerPlan string, salt string, opponentPlan string, found bool)ChallengeProof returns everything a third party needs to re-derive a finished battle from scratch, without trusting this realm, either player, or Gnotchi's server. This realm's entire reason to exist is that nobody has to be trusted about the result. ChallengeStatus returns the result -- which is this realm ASSERTING a winner. That is exactly the kind of claim the realm was built to avoid having to make. Given commit, challengerPlan, salt and opponentPlan, anyone can check sha256(challengerPlan + ":" + salt) == commit, feed both plans through app/js/rules.js's resolveBattle, and get the same winner and the same puff -- or catch this realm being wrong. Without these four fields the "you don't have to trust us" claim is itself something you'd have to take on trust, and since a realm cannot be edited, a getter missing at deploy time stays missing until a v2 and a migration. Before a Reveal, challengerPlan and salt are empty strings, so this getter cannot leak a plan early: there is no plan here to leak until the challenger has published it themselves. commit is readable from the moment of Challenge, but it always was -- it is a public argument of that transaction -- so exposing it here reveals nothing new. THE SEAL'S STRENGTH IS THE SALT'S, AND THIS REALM CANNOT ENFORCE THAT. Stated plainly because the honest version is narrower than it first looks. There are only 27 possible plans: battleRounds is 3 and there are 3 legal moves, so 3^3. commitDigest is a single bare sha256 with no KDF and no iteration. So a commit whose salt is known, reused, or derived from anything predictable falls to 27 hashes -- a complete break of the only thing this realm exists to guarantee. Reveal deliberately imposes no minimum salt length, because a length check added here would permanently reject any challenge already sealed with a shorter salt and force its challenger to forfeit. The entropy requirement therefore lives entirely in the client: app/js/battle.js seals with 32 fresh crypto.getRandomValues bytes per plan, never reused. Any other client MUST do the same; 27 candidates is not a margin to be casual with. The preimage also binds neither the challenge ID nor the challenger's address, so the same commit can be posted in several challenges. No advantage was found in doing so -- a replayer commits to a plan they will not learn until the original reveals, which is strictly worse than choosing one -- but it is recorded here because the preimage has to match app/js/rules.js byte for byte and so can never be changed. A forfeited challenge has no challengerPlan by definition -- the challenger never revealed one, which is why it was forfeited. \`found\` is false for an unknown ID.
- OID
- 05791d…1039:25
ChallengeProof details
Render
func(path string) stringRender: "" shows the realm's totals; any other path is treated as a challengeID.
- OID
- 05791d…1039:26
Render details
mustChallenge
func(id string) *v1.arenaChallenge- OID
- 05791d…1039:27
mustChallenge details
hasGnome
func(addr .uverse.address) bool- OID
- 05791d…1039:28
hasGnome details
validCommitHex
func(s string) boolvalidCommitHex checks only the SHAPE of a posted commitHash (64 lowercase hex chars, matching the exact output of both Node's crypto.createHash('sha256').digest('hex') and this file's own commitDigest) -- an early, player-facing failure, not a correctness requirement: Reveal's own byte-for-byte comparison is what actually enforces the seal.
- OID
- 05791d…1039:29
validCommitHex details
validChallengeID
func(s string) boolvalidChallengeID accepts only what Challenge itself can mint: a seqid.ID rendered by cford32, which is Crockford base32 and therefore a strict subset of lowercase alphanumerics. 16 is a deliberate ceiling rather than an exact width -- seqid's encoded width grows with the counter, so pinning one number here would start rejecting real IDs later, and this realm cannot be edited once deployed. Nothing outside this set can ever be a key in \`challenges\`, so narrowing to it loses no lookup.
- OID
- 05791d…1039:30
validChallengeID details
commitDigest
func(planString string, saltHex string) stringcommitDigest ports app/js/rules.js's commitHash exactly: sha256(planString + ":" + saltHex), hex-encoded. See the package doc for the cross-checked proof this matches Node's crypto output byte for byte.
- OID
- 05791d…1039:31
commitDigest details
parsePlan
func(s string) []stringparsePlan ports app/js/rules.js's parsePlan: exactly battleRounds comma-separated moves, each trimmed and uppercased to one of Q, S or G.
- OID
- 05791d…1039:32
parsePlan details
planFormatError
func() string- OID
- 05791d…1039:33
planFormatError details
styleBeats
func(a string, b string) bool- OID
- 05791d…1039:34
styleBeats details
typeBeats
func(a string, b string) booltypeBeats: empty string (no type assigned yet) never beats, and is never beaten by, anything -- ports app/js/rules.js's typeBeats: \`!!a && !!b && TYPE\_BEATS\[a] === b\`.
- OID
- 05791d…1039:35
typeBeats details
battleResult
typeValue
v1.battleResult
resolveBattle
func(challengerMoves []string, challengerType string, opponentMoves []string, opponentType string, puff int64) v1.battleResultresolveBattle ports app/js/rules.js's resolveBattle exactly: no randomness, a knock-out ends the fight early, the round's winner hits for winHit plus typeEdge with the type edge, a tie costs both battleTieLoss. puff is an explicit parameter (not just the package's own battlePuff constant) purely so tests can mirror rules.test.js's own trick of lowering it to force an early knock-out within three rounds -- every real call from Reveal always passes battlePuff.
- OID
- 05791d…1039:36
resolveBattle details
clampPuff
func(v int64) int64- OID
- 05791d…1039:37
clampPuff details
dayNumber
func(unixSeconds int64) int64- OID
- 05791d…1039:38
dayNumber details
weekNumber
func(unixSeconds int64) int64- OID
- 05791d…1039:39
weekNumber details
dayKey
func(addr .uverse.address, now int64) string- OID
- 05791d…1039:40
dayKey details
pairKey
func(a .uverse.address, b .uverse.address) stringpairKey is symmetric: challenger-vs-opponent and opponent-vs-challenger must land on the same weekly counter.
- OID
- 05791d…1039:41
pairKey details
weekKey
func(pk string, now int64) string- OID
- 05791d…1039:42
weekKey details
getCounter
func(tree *v0.Tree, key string) int64- OID
- 05791d…1039:43
getCounter details
incCounter
func(tree *v0.Tree, key string)- OID
- 05791d…1039:44
incCounter details
rankedEligible
func(challenger .uverse.address, opponent .uverse.address, challengerLevel int64, opponentLevel int64, now int64) boolrankedEligible: level 3 to rank (xp.rankedFromLevel), at most rankedPerGnomePerDay ranked results a gnome a day, at most rankedGamesPerPairPerWeek a pair a week. Never blocks a battle -- see the const block's own comment for why this is a downgrade, not a refusal. KNOWN AND PERMANENT: THE PLAYER CHOOSES WHEN THIS RUNS, SO THEY CHOOSE WHAT IT SEES. Every input below -- both daily counters, both pair counters, both levels -- is read live at Reveal (or ClaimForfeit) time, and that moment is picked by the revealer, who by then already knows the outcome from the opponent's cleartext plan. A challenger holding several accepted challenges can reveal their three wins first, exhausting their own rankedPerGnomePerDay allowance, after which every loss revealed that day is recorded ranked = false. Free, repeatable daily, and invisible in the stored record. There is no fix available inside this function. Deciding eligibility at Challenge time instead would just move the choice to a moment the challenger controls even more completely, and the counters cannot be consumed before the battle exists. A real fix needs the plan asymmetry to go (both sides sealed) or an ordering the players do not pick -- either of which is a different realm, not a change to this line. Nothing reads \`ranked\` today: season/v1 does not exist (see the SCOPE note in the package doc). That bounds the harm to zero right now and is precisely why it is written down here instead of deferred -- whoever builds season/v1 will be reading an immutable field whose distribution is skewed in the ranked player's favour, and they should learn that from this comment rather than from the leaderboard.
- OID
- 05791d…1039:45
rankedEligible details
recordRanked
func(challenger .uverse.address, opponent .uverse.address, now int64)- OID
- 05791d…1039:46