earnings.gno
5.81 Kb · 191 lines
1package arena
2
3import "gno.land/p/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/payout/v0"
4
5// Earnings are what players won, added up round after round. Like the
6// results they are a record for readers: no payment is ever computed from
7// them.
8//
9// For one settled round a player's winnings are what they were paid above
10// their own stake, and never less than zero: losing a stake does not take
11// anything back from what was won before. Cancelled and refunded rounds
12// count for nothing, nobody won or lost in them.
13
14// topSize is how many players the list of top winners holds.
15const topSize = 10
16
17// totals is what the realm remembers about one player.
18type totals struct {
19 won int64 // ugnot won above the stakes, over every settled round
20 rounds int64 // settled rounds played
21}
22
23// Totals are stored as a single string, "won:rounds", for the same reason
24// as the pilots of the game realm: a struct would be one more stored object
25// per player, and the referee pays for up to 16 new players per settlement.
26
27// encode returns the stored form of t.
28func (t totals) encode() string {
29 return itoa(t.won) + ":" + itoa(t.rounds)
30}
31
32// totalsOf returns the totals of a player, zero for a player who never
33// finished a round.
34func totalsOf(addr address) totals {
35 v := earnings.Get(addr.String())
36 if v == nil {
37 return totals{}
38 }
39 s := v.(string)
40 colon := indexFrom(s, 0, ':')
41 if colon == len(s) {
42 panic(errCorruptEarnings)
43 }
44 won, ok := digits(s[:colon])
45 if !ok {
46 panic(errCorruptEarnings)
47 }
48 rounds, ok := digits(s[colon+1:])
49 if !ok {
50 panic(errCorruptEarnings)
51 }
52 return totals{won: won, rounds: rounds}
53}
54
55// creditRound records a settled round for each of its players: one more
56// round played, and what they were paid above their stake, if anything.
57func creditRound(stake int64, players []address, payouts []payout.Payout) {
58 for _, p := range players {
59 gain := int64(0)
60 for _, paid := range payouts {
61 if paid.Address == p && paid.Amount > stake {
62 gain = paid.Amount - stake
63 }
64 }
65
66 t := totalsOf(p)
67 before := t.won
68 t.won += gain
69 t.rounds++
70 earnings.Set(p.String(), t.encode())
71
72 if gain > 0 {
73 if next, changed := winners.raise(p, before, t.won, topSize); changed {
74 winners = next
75 }
76 }
77 }
78}
79
80// ranking is the list of top winners: the players who won the most, best
81// first, at most topSize lines and one line per player. With equal winnings,
82// the player who got there first stays ahead.
83//
84// It is the ranking of the game realm. It is kept sorted at write time, so
85// reading it never has to look at every player. Winnings only going up is
86// what makes that exact: a player outside the list can only enter it through
87// raise.
88//
89// The two slices are parallel: slices of plain values are one stored object
90// each, where a slice of structs would be one object per line.
91type ranking struct {
92 addrs []address
93 scores []int64
94}
95
96// raise puts a player at their place after their score went up from old to
97// score. It returns the ranking to store, and changed == false when it is
98// left as it is: the player is not on a full list and still not good enough
99// to get on.
100func (r ranking) raise(addr address, old, score int64, size int) (next ranking, changed bool) {
101 from := r.lineOf(addr, old)
102 to := r.search(score - 1) // after every line with this score or more
103 if from < 0 && to >= size {
104 return r, false
105 }
106 if from >= 0 && to > from {
107 to = from // cannot happen: a score only goes up
108 }
109 next = ranking{
110 addrs: spliceAddrs(r.addrs, from, to, addr, size),
111 scores: spliceInts(r.scores, from, to, score, size),
112 }
113 return next, true
114}
115
116// search returns the index of the first line with a score of limit or less,
117// the number of lines when there is none. The list is sorted, so this is a
118// binary search.
119func (r ranking) search(limit int64) int {
120 lo, hi := 0, len(r.scores)
121 for lo < hi {
122 mid := (lo + hi) / 2
123 if r.scores[mid] <= limit {
124 hi = mid
125 } else {
126 lo = mid + 1
127 }
128 }
129 return lo
130}
131
132// lineOf returns the index of a player's line, -1 when they have none. old
133// is the player's score before the change being applied.
134//
135// A line always carries the current score of its player, because every
136// change of a score goes through raise. So the line can only be among those
137// with exactly old. A player whose score was zero has never been through
138// raise and has no line.
139func (r ranking) lineOf(addr address, old int64) int {
140 if old <= 0 {
141 return -1
142 }
143 for i := r.search(old); i < len(r.scores) && r.scores[i] == old; i++ {
144 if r.addrs[i] == addr {
145 return i
146 }
147 }
148 return -1
149}
150
151// spliceAddrs returns a copy of a ranking column with v inserted at index
152// to. With from >= 0 the element at that index, the player's previous line,
153// is left out; to must not be past it. With from < 0 the result is kept to
154// size elements by dropping the tail; to must be below size.
155//
156// The copy has exactly the length of its content, so that nothing else stays
157// in storage.
158func spliceAddrs(list []address, from, to int, v address, size int) []address {
159 if from < 0 {
160 if len(list) >= size {
161 list = list[:size-1]
162 }
163 out := make([]address, 0, len(list)+1)
164 out = append(out, list[:to]...)
165 out = append(out, v)
166 return append(out, list[to:]...)
167 }
168 out := make([]address, 0, len(list))
169 out = append(out, list[:to]...)
170 out = append(out, v)
171 out = append(out, list[to:from]...)
172 return append(out, list[from+1:]...)
173}
174
175// spliceInts is spliceAddrs for a column of numbers.
176func spliceInts(list []int64, from, to int, v int64, size int) []int64 {
177 if from < 0 {
178 if len(list) >= size {
179 list = list[:size-1]
180 }
181 out := make([]int64, 0, len(list)+1)
182 out = append(out, list[:to]...)
183 out = append(out, v)
184 return append(out, list[to:]...)
185 }
186 out := make([]int64, 0, len(list))
187 out = append(out, list[:to]...)
188 out = append(out, v)
189 out = append(out, list[to:from]...)
190 return append(out, list[from+1:]...)
191}