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

tally.gno

4.71 Kb · 153 lines
  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, because
  4// gno has no sort.Slice and every leaderboard therefore needs a named type
  5// with Len, Less and Swap. Eight of them rank on ONE score and then break the
  6// tie, which is what this package is; the rest rank on two scores and keep
  7// their own comparator. They do not agree on what happens at a tie, and one of
  8// them does not break ties at all.
  9//
 10// # The tie-break is the point
 11//
 12// A Render that reshuffles between identical calls is a bug, so the final
 13// comparison must be a total order. Comparing the score again (as
 14// r/moul/x/daily/rpgroom's third clause does) is not one: two rows with equal
 15// scores compare false in both directions, and their order then falls out of
 16// whatever the sort algorithm did with the input, which is reproducible but
 17// nobody chose it. [Sort] always ends on the key, so equal scores render in a
 18// stable, explicable order.
 19//
 20// # What is not here
 21//
 22// Storage: the caller keeps its own avl tree and maps it to [Entry]. That is
 23// what makes this adoptable by realms that already have their state, and
 24// [Board] is for new ones that do not.
 25//
 26// Rendering: a podium and a table already have an owner in
 27// [p/moul/kit/ui](/p/moul/kit/ui/v0).
 28package tally
 29
 30import (
 31	"sort"
 32
 33	"gno.land/p/nt/avl/v0"
 34)
 35
 36// 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   string
 42	Score int64
 43}
 44
 45type byScore []Entry
 46
 47func (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].Score
 52	}
 53	return e[i].Key < e[j].Key
 54}
 55
 56// Sort orders entries in place: highest score first, ties by key ascending.
 57func Sort(entries []Entry) { sort.Sort(byScore(entries)) }
 58
 59// Top returns the n highest entries, sorted. A n at or below zero returns
 60// 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 nil
 64	}
 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}
 73
 74// Rank returns the 1-based position of key once entries are sorted, or 0 if
 75// the key is absent. Equal scores still get distinct ranks, decided by the
 76// 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 + 1
 84		}
 85	}
 86	return 0
 87}
 88
 89// Board is a scoreboard that owns its storage, for a realm that does not
 90// already have one. It is backed by a tree rather than a map: gno map
 91// iteration order is unspecified, and a Render built on one is not
 92// reproducible.
 93type Board struct {
 94	scores *avl.Tree // key -> int64
 95}
 96
 97// NewBoard returns an empty Board.
 98func NewBoard() *Board { return &Board{scores: avl.NewTree()} }
 99
100// Add increases key's score by delta, which may be negative, and returns the
101// new score. A key that was absent starts at zero.
102func (b *Board) Add(key string, delta int64) int64 {
103	n := b.Score(key) + delta
104	b.scores.Set(key, n)
105	return n
106}
107
108// Set replaces key's score outright.
109func (b *Board) Set(key string, score int64) { b.scores.Set(key, score) }
110
111// Score returns key's score, or zero if it has none. It does not distinguish
112// 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 0
117	}
118	return v.(int64)
119}
120
121// Has reports whether key has been scored at all.
122func (b *Board) Has(key string) bool { return b.scores.Get(key) != nil }
123
124// Remove drops key, reporting whether it was there.
125func (b *Board) Remove(key string) bool {
126	_, removed := b.scores.Remove(key)
127	return removed
128}
129
130// Len is the number of scored keys.
131func (b *Board) Len() int { return b.scores.Size() }
132
133// 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 false
139	})
140	Sort(out)
141	return out
142}
143
144// Top returns the n highest rows, sorted.
145func (b *Board) Top(n int) []Entry { return Top(b.Entries(), n) }
146
147// 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 0
151	}
152	return Rank(b.Entries(), key)
153}