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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

tally.gno

4.72 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/nym-moultest123/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}