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

v0 source pure

Package tally orders a scoreboard: highest score first, ties broken by key.

Readme View source

gno.land/p/moul/kit/tally

Order 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.

Why it exists

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.

What the 16 disagree on: the tie

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.

What is here

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.

What is deliberately not here

  • Rendering. A podium, a table and address shortening belong to kit/ui. This package returns rows.
  • Scores that are not int64. Nothing on chain ranks by a float, and gno has no float determinism worth relying on for consensus output.
  • Stable sort. 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:

gno.land/p/moul/kit/tally/v0 dependency graph

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.

Overview

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.

The tie-break is the point

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.

What is not here

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).

Functions 4

func Rank

1func Rank(entries []Entry, key string) int
source

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.

func Sort

1func Sort(entries []Entry)
source

Sort orders entries in place: highest score first, ties by key ascending.

func NewBoard

1func NewBoard() *Board
source

NewBoard returns an empty Board.

func Top

1func Top(entries []Entry, n int) []Entry
source

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.

Types 2

type Board

struct
1type Board struct {
2	scores *avl.Tree // key -> int64
3}
source

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.

Methods on Board

func Add

method on Board
1func (b *Board) Add(key string, delta int64) int64
source

Add increases key's score by delta, which may be negative, and returns the new score. A key that was absent starts at zero.

func Entries

method on Board
1func (b *Board) Entries() []Entry
source

Entries returns every row, sorted by Sort.

func Has

method on Board
1func (b *Board) Has(key string) bool
source

Has reports whether key has been scored at all.

func Len

method on Board
1func (b *Board) Len() int
source

Len is the number of scored keys.

func Rank

method on Board
1func (b *Board) Rank(key string) int
source

Rank returns key's 1-based position, or 0 if it is not on the board.

func Remove

method on Board
1func (b *Board) Remove(key string) bool
source

Remove drops key, reporting whether it was there.

func Score

method on Board
1func (b *Board) Score(key string) int64
source

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.

func Set

method on Board
1func (b *Board) Set(key string, score int64)
source

Set replaces key's score outright.

func Top

method on Board
1func (b *Board) Top(n int) []Entry
source

Top returns the n highest rows, sorted.

type Entry

struct
1type Entry struct {
2	Key   string
3	Score int64
4}
source

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.

Imports 2

Source Files 4