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.

profiles.gno

5.56 Kb · 213 lines
  1// Package profiles keeps the Gnoslinger players' records (games played,
  2// victory points) and their ranking apart from the game realm, so a new
  3// version of the game (new rules) keeps the scores. Only the game realms on
  4// the allowlist may write; the admin, the deployer, manages that list.
  5package profiles
  6
  7import (
  8	"chain"
  9	"chain/runtime/unsafe"
 10	"strconv"
 11	"strings"
 12	"time"
 13
 14	"gno.land/p/nt/avl/v0"
 15	"gno.land/r/sys/users"
 16)
 17
 18// MaxPage bounds PlayersJSON and json/players/{offset}/{count}.
 19const MaxPage = 50
 20
 21// Profile is a player's record, created the first time the address creates
 22// or joins a game.
 23type Profile struct {
 24	Addr address
 25	// GamesPlayed counts the games the player sat in once they started (a
 26	// second player joined); a cancelled showdown does not count.
 27	GamesPlayed int
 28	// VictoryPoints adds up the points of the player's finished games (rule
 29	// 15); it may be negative.
 30	VictoryPoints int
 31	CreatedAt     time.Time
 32}
 33
 34var (
 35	// admin manages the allowlist of game realms; the deployer at first.
 36	admin    = unsafe.OriginCaller()
 37	profiles = avl.NewTree() // address -> *Profile
 38	// ranking orders the profiles, best first: rankKey -> *Profile.
 39	ranking = avl.NewTree()
 40	// games is the allowlist: the pkgpaths of the game realms that may write.
 41	games = avl.NewTree()
 42)
 43
 44func init() {
 45	games.Set("gno.land/r/g1th0masp67zfs9lg6d34qhghldhg6kjcqjuv7c2/gnoslinger/v0", true)
 46}
 47
 48// --- Writes, from an allowed game realm ---
 49
 50// Ensure creates addr's profile if it has none yet.
 51func Ensure(cur realm, addr address) {
 52	assertGame(cur)
 53	ensure(addr)
 54}
 55
 56// AddGamePlayed counts a game that started for addr.
 57func AddGamePlayed(cur realm, addr address) {
 58	assertGame(cur)
 59	update(addr, func(p *Profile) { p.GamesPlayed++ })
 60}
 61
 62// AddVictoryPoints adds an award's points (< 0 for a loss) to addr's total.
 63func AddVictoryPoints(cur realm, addr address, points int) {
 64	assertGame(cur)
 65	update(addr, func(p *Profile) { p.VictoryPoints += points })
 66}
 67
 68func assertGame(cur realm) {
 69	if !cur.IsCurrent() {
 70		panic("profiles: realm value is not the caller's live cur")
 71	}
 72	if caller := cur.Previous().PkgPath(); !IsGame(caller) {
 73		panic("profiles: " + caller + " is not an allowed game realm")
 74	}
 75}
 76
 77// --- Admin ---
 78
 79// Allow lets the game realm pkgPath write the profiles (a new version of
 80// the game).
 81func Allow(cur realm, pkgPath string) {
 82	assertAdmin(cur)
 83	if !strings.HasPrefix(pkgPath, "gno.land/r/") {
 84		panic("profiles: " + pkgPath + " is not a realm path")
 85	}
 86	games.Set(pkgPath, true)
 87	chain.Emit("GameAllowed", "pkgpath", pkgPath)
 88}
 89
 90// Disallow stops pkgPath from writing (a game version no longer scored);
 91// its calls panic from then on.
 92func Disallow(cur realm, pkgPath string) {
 93	assertAdmin(cur)
 94	if _, removed := games.Remove(pkgPath); !removed {
 95		panic("profiles: " + pkgPath + " is not allowed")
 96	}
 97	chain.Emit("GameDisallowed", "pkgpath", pkgPath)
 98}
 99
100// SetAdmin hands the allowlist over to addr.
101func SetAdmin(cur realm, addr address) {
102	assertAdmin(cur)
103	if !addr.IsValid() {
104		panic("profiles: invalid address")
105	}
106	admin = addr
107	chain.Emit("AdminChanged", "admin", addr.String())
108}
109
110func assertAdmin(cur realm) {
111	if !cur.IsCurrent() {
112		panic("profiles: realm value is not the caller's live cur")
113	}
114	if prev := cur.Previous(); !prev.IsUser() || prev.Address() != admin {
115		panic("profiles: admin only")
116	}
117}
118
119// --- Reads ---
120
121// Admin is the address managing the allowlist.
122func Admin() address { return admin }
123
124// IsGame tells whether the realm pkgPath may write the profiles.
125func IsGame(pkgPath string) bool { return games.Has(pkgPath) }
126
127// Games lists the allowed game realms.
128func Games() []string {
129	var l []string
130	games.Iterate("", "", func(k string, _ any) bool {
131		l = append(l, k)
132		return false
133	})
134	return l
135}
136
137// Get is a copy of addr's profile, false when it has none.
138func Get(addr address) (Profile, bool) {
139	v := profiles.Get(addr.String())
140	if v == nil {
141		return Profile{}, false
142	}
143	return *v.(*Profile), true
144}
145
146// Rank is addr's 1-based position in the ranking, 0 without a profile.
147func Rank(addr address) int {
148	v := profiles.Get(addr.String())
149	if v == nil {
150		return 0
151	}
152	return rankOf(v.(*Profile))
153}
154
155// Count is the number of profiles.
156func Count() int { return profiles.Size() }
157
158// Name is addr's name in gno.land/r/sys/users (where namereg registers
159// them), or "".
160func Name(addr address) string {
161	if addr == "" {
162		return ""
163	}
164	if u := users.ResolveAddress(addr); u != nil {
165		return u.Name()
166	}
167	return ""
168}
169
170// --- Ranking ---
171
172// rankKey sorts by victory points, then games played, both descending, then
173// by address.
174func rankKey(p *Profile) string {
175	return desc(p.VictoryPoints) + ":" + desc(p.GamesPlayed) + ":" + p.Addr.String()
176}
177
178// desc is a fixed-width key that sorts larger numbers first; victory
179// points may be negative, so it is centred on 0 (|n| below 5e8).
180func desc(n int) string {
181	s := strconv.Itoa(500000000 - n)
182	return strings.Repeat("0", 9-len(s)) + s
183}
184
185// ensure returns addr's profile, creating it on first use.
186func ensure(addr address) *Profile {
187	if v := profiles.Get(addr.String()); v != nil {
188		return v.(*Profile)
189	}
190	p := &Profile{Addr: addr, CreatedAt: time.Now()}
191	profiles.Set(addr.String(), p)
192	ranking.Set(rankKey(p), p)
193	return p
194}
195
196// update applies change to addr's profile and keeps its rank.
197func update(addr address, change func(p *Profile)) {
198	p := ensure(addr)
199	ranking.Remove(rankKey(p))
200	change(p)
201	ranking.Set(rankKey(p), p)
202}
203
204// rankOf is the 1-based position of p in the ranking.
205func rankOf(p *Profile) int {
206	key := rankKey(p)
207	rank := 0
208	ranking.Iterate("", "", func(k string, _ any) bool {
209		rank++
210		return k == key
211	})
212	return rank
213}