1// Package tally orders a scoreboard: highest score first, ties broken by key.2//3// Sixteen realms in this repo carry their own sort.Interface triple, because4// gno has no sort.Slice and every leaderboard therefore needs a named type5// with Len, Less and Swap. Eight of them rank on ONE score and then break the6// tie, which is what this package is; the rest rank on two scores and keep7// their own comparator. They do not agree on what happens at a tie, and one of8// them does not break ties at all.9//10// # The tie-break is the point11//12// A Render that reshuffles between identical calls is a bug, so the final13// comparison must be a total order. Comparing the score again (as14// r/moul/x/daily/rpgroom's third clause does) is not one: two rows with equal15// scores compare false in both directions, and their order then falls out of16// whatever the sort algorithm did with the input, which is reproducible but17// nobody chose it. [Sort] always ends on the key, so equal scores render in a18// stable, explicable order.19//20// # What is not here21//22// Storage: the caller keeps its own avl tree and maps it to [Entry]. That is23// what makes this adoptable by realms that already have their state, and24// [Board] is for new ones that do not.25//26// Rendering: a podium and a table already have an owner in27// [p/moul/kit/ui](/p/moul/kit/ui/v0).28package tally2930import (31 "sort"3233 "gno.land/p/nt/avl/v0"34)3536// Entry is one row of a scoreboard: an opaque key and its score.37//38// Key is whatever the caller ranks by, usually an address in its String form.39// It is the tie-break, so it must be unique within one board.40type Entry struct {41 Key string42 Score int6443}4445type byScore []Entry4647func (e byScore) Len() int { return len(e) }48func (e byScore) Swap(i, j int) { e[i], e[j] = e[j], e[i] }49func (e byScore) Less(i, j int) bool {50 if e[i].Score != e[j].Score {51 return e[i].Score > e[j].Score52 }53 return e[i].Key < e[j].Key54}5556// Sort orders entries in place: highest score first, ties by key ascending.57func Sort(entries []Entry) { sort.Sort(byScore(entries)) }5859// Top returns the n highest entries, sorted. A n at or below zero returns60// nothing; a n past the end returns everything. The input is not modified.61func Top(entries []Entry, n int) []Entry {62 if n <= 0 || len(entries) == 0 {63 return nil64 }65 out := make([]Entry, len(entries))66 copy(out, entries)67 Sort(out)68 if n > len(out) {69 n = len(out)70 }71 return out[:n]72}7374// Rank returns the 1-based position of key once entries are sorted, or 0 if75// the key is absent. Equal scores still get distinct ranks, decided by the76// same key tie-break Sort uses, so no two rows ever claim the same place.77func Rank(entries []Entry, key string) int {78 sorted := make([]Entry, len(entries))79 copy(sorted, entries)80 Sort(sorted)81 for i, e := range sorted {82 if e.Key == key {83 return i + 184 }85 }86 return 087}8889// Board is a scoreboard that owns its storage, for a realm that does not90// already have one. It is backed by a tree rather than a map: gno map91// iteration order is unspecified, and a Render built on one is not92// reproducible.93type Board struct {94 scores *avl.Tree // key -> int6495}9697// NewBoard returns an empty Board.98func NewBoard() *Board { return &Board{scores: avl.NewTree()} }99100// Add increases key's score by delta, which may be negative, and returns the101// new score. A key that was absent starts at zero.102func (b *Board) Add(key string, delta int64) int64 {103 n := b.Score(key) + delta104 b.scores.Set(key, n)105 return n106}107108// Set replaces key's score outright.109func (b *Board) Set(key string, score int64) { b.scores.Set(key, score) }110111// Score returns key's score, or zero if it has none. It does not distinguish112// an absent key from one scoring zero; use [Board.Has] when that matters.113func (b *Board) Score(key string) int64 {114 v := b.scores.Get(key)115 if v == nil {116 return 0117 }118 return v.(int64)119}120121// Has reports whether key has been scored at all.122func (b *Board) Has(key string) bool { return b.scores.Get(key) != nil }123124// Remove drops key, reporting whether it was there.125func (b *Board) Remove(key string) bool {126 _, removed := b.scores.Remove(key)127 return removed128}129130// Len is the number of scored keys.131func (b *Board) Len() int { return b.scores.Size() }132133// Entries returns every row, sorted by [Sort].134func (b *Board) Entries() []Entry {135 out := make([]Entry, 0, b.scores.Size())136 b.scores.Iterate("", "", func(k string, v any) bool {137 out = append(out, Entry{Key: k, Score: v.(int64)})138 return false139 })140 Sort(out)141 return out142}143144// Top returns the n highest rows, sorted.145func (b *Board) Top(n int) []Entry { return Top(b.Entries(), n) }146147// Rank returns key's 1-based position, or 0 if it is not on the board.148func (b *Board) Rank(key string) int {149 if !b.Has(key) {150 return 0151 }152 return Rank(b.Entries(), key)153}154Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.