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}