func Rank
Rank returns the 1-based position of key once entries are sorted, or 0 if the key is absent. Equal scores still get distinct ranks, decided by the same key tie-break Sort uses, so no two rows ever claim the same place.
Package tally orders a scoreboard: highest score first, ties broken by key.
gno.land/p/moul/kit/tallyOrder a scoreboard: highest score first, ties broken by key.
Fourth package of the p/moul/kit/* layer
(moul/gno-contracts#151), after
ui, store and num. kit composes the existing
packages, it does not replace them.
gno has no sort.Slice. Ordering anything therefore needs a named type with
Len, Less and Swap, so every realm that shows a leaderboard writes one.
Measured on main, 2026-09-28: 16 packages carry their own sort.Interface
triple. They are not all the same shape, so here is the honest split, by how
many score clauses the comparator has before its final return:
| shape | packages | fits this package? |
|---|---|---|
| one score, then a tie-break | kudos, leaderboard, multiset, pixelcanvas, prorata, reactions, streak, tamagotchi |
yes, 8 of the 16 |
| two scores, then a tie-break | collatz, idle, kingofdice, rpgroom, streaks, tipjar |
no |
| not a scoreboard | eggling, semverdemo |
no |
So this package targets 8, and it says so rather than claiming 16. A
two-score board (idle ranks on prestige, then lifetime, then owner) has a
genuinely domain-specific comparator, and flattening it into one int64 would
be a worse API than the triple it replaced.
The final clause of Less is what decides two equal scores, and it is the
clause everyone writes differently:
| final clause | realms |
|---|---|
the key, as addr.String() |
idle, kingofdice, leaderboard, streaks, tamagotchi, tipjar |
| the key, as a bare string | kudos, pixelcanvas, streak |
a domain field (Elem, N, emoji, index) |
multiset, collatz, reactions, prorata |
| the score again | rpgroom |
rpgroom's third clause is return r[i].Kills > r[j].Kills, after Level and
XP. That is not a total order: two heroes equal on all three compare
false in both directions, so their order is whatever the sort algorithm did
with the input. It is reproducible (heroes.Iterate walks an avl tree, and
sort.Sort is deterministic), so this is not a consensus bug, but nobody chose
that order and nothing pins it.
Sort always ends on Key, so the answer is the same one every time and it is
explicable to a reader.
Sort(entries) |
in place, highest first, ties by key |
Top(entries, n) |
the n highest, copied, so the caller's slice is untouched |
Rank(entries, key) |
1-based, 0 if absent, and never a tie: equal scores still get distinct places |
Board |
NewBoard, Add, Set, Score, Has, Remove, Len, Entries, Top, Rank |
Two entry points on purpose. The 16 above already own their state and adopt by
mapping it to []Entry; a realm starting fresh reaches for Board and gets the
storage too.
Board is backed by an avl tree, not a map. gno map iteration order is
unspecified, so a Render built by ranging a map is not reproducible.
TestBoardOrderDoesNotDependOnInsertion pins it.
kit/ui. This package returns rows.int64. Nothing on chain ranks by a float, and gno
has no float determinism worth relying on for consensus output.Sort is a total order, so stability is not observable.Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.
Package tally orders a scoreboard: highest score first, ties broken by key.
Sixteen realms in this repo carry their own sort.Interface triple, because gno has no sort.Slice and every leaderboard therefore needs a named type with Len, Less and Swap. Eight of them rank on ONE score and then break the tie, which is what this package is; the rest rank on two scores and keep their own comparator. They do not agree on what happens at a tie, and one of them does not break ties at all.
A Render that reshuffles between identical calls is a bug, so the final comparison must be a total order. Comparing the score again (as r/moul/x/daily/rpgroom's third clause does) is not one: two rows with equal scores compare false in both directions, and their order then falls out of whatever the sort algorithm did with the input, which is reproducible but nobody chose it. Sort always ends on the key, so equal scores render in a stable, explicable order.
Storage: the caller keeps its own avl tree and maps it to Entry. That is what makes this adoptable by realms that already have their state, and Board is for new ones that do not.
Rendering: a podium and a table already have an owner in p/moul/kit/ui(/p/moul/kit/ui/v0).
Rank returns the 1-based position of key once entries are sorted, or 0 if the key is absent. Equal scores still get distinct ranks, decided by the same key tie-break Sort uses, so no two rows ever claim the same place.
Sort orders entries in place: highest score first, ties by key ascending.
NewBoard returns an empty Board.
Top returns the n highest entries, sorted. A n at or below zero returns nothing; a n past the end returns everything. The input is not modified.
Board is a scoreboard that owns its storage, for a realm that does not already have one. It is backed by a tree rather than a map: gno map iteration order is unspecified, and a Render built on one is not reproducible.
Add increases key's score by delta, which may be negative, and returns the new score. A key that was absent starts at zero.
Entries returns every row, sorted by Sort.
Has reports whether key has been scored at all.
Len is the number of scored keys.
Rank returns key's 1-based position, or 0 if it is not on the board.
Remove drops key, reporting whether it was there.
Score returns key's score, or zero if it has none. It does not distinguish an absent key from one scoring zero; use Board.Has when that matters.
Set replaces key's score outright.
Top returns the n highest rows, sorted.
Entry is one row of a scoreboard: an opaque key and its score.
Key is whatever the caller ranks by, usually an address in its String form. It is the tie-break, so it must be unique within one board.