query.gno
7.59 Kb · 262 lines
1package arena
2
3import (
4 "encoding/base64"
5 "strconv"
6 "strings"
7 "time"
8
9 "gno.land/p/g188vafzutzvlfw6wt7u3s62xtuvm0epmu8ufmfz/gnomepit/engine"
10 "gno.land/p/nt/avl/v0"
11 "gno.land/r/sys/users"
12)
13
14// MatchSummary is one match as a player's list shows it. Round is the round being
15// played, or the last one resolved once the match is over. ToMove is the address that
16// must move next, empty when none. Deadline is the end of the current move's 24 hours,
17// or the expiry of an open challenge, in Unix seconds. Opponent is the address an open
18// challenge is reserved for, "" when anyone can accept it and once accepted.
19type MatchSummary struct {
20 ID, Phase string
21 Round int
22 Challenger, Acceptor string
23 ChallengerClan, AcceptorClan string
24 ToMove string
25 Deadline int64
26 Result, Reason string
27 Opponent string
28}
29
30// MatchView is everything one match screen needs. Plans holds [challenger, acceptor]
31// for each resolved round, and for the round being played while the match runs, ""
32// while a plan is sealed or not yet made; HP holds both sides' HP after each resolved
33// round; Box is the challenger's encrypted pending plan, in base64.
34type MatchView struct {
35 MatchSummary
36 ChallengerDeck, AcceptorDeck string
37 Plans [][]string
38 Box string
39 HP [][]int
40 DeadlinePassed bool
41}
42
43// SessionLock binds the session key at address Session to the match whose id is Match.
44// Expires is the session's expiry in Unix seconds, 0 for none.
45type SessionLock struct {
46 Session, Match string
47 Expires int64
48}
49
50// Standing is one player's place on the ladder. Rank is 1 for the highest rating and
51// equal ratings share it; Rank is 0 and Rating 1000 for a player with no rated match.
52// Name is the player's gno.land name, "" without one. MainClan is "" until their
53// first rated match.
54type Standing struct {
55 Rank int
56 Player, Name, MainClan string
57 Rating int
58 Wins, Losses, Draws int
59}
60
61// ClanStanding is one clan's record across rated cross-clan matches.
62type ClanStanding struct {
63 Rank int
64 Clan string
65 Rating int
66 Wins, Losses, Draws int
67}
68
69// Leaderboard lists rated players, highest rating first: limit of them after skipping offset.
70func Leaderboard(offset, limit int) []Standing {
71 checkPage(offset, limit)
72 rules := engine.ProofOfConcept()
73 list := []Standing{}
74 ladder.TraverseByOffset(offset, limit, true, true, func(n *avl.Node) bool {
75 s := standingOf(rules, address(n.Key()[2:]))
76 switch {
77 case len(list) == 0:
78 s.Rank = rankOf(s.Rating)
79 case list[len(list)-1].Rating == s.Rating:
80 s.Rank = list[len(list)-1].Rank
81 default:
82 s.Rank = offset + len(list) + 1
83 }
84 list = append(list, s)
85 return false
86 })
87 return list
88}
89
90// StandingOf returns player's standing, rated or not.
91func StandingOf(player address) Standing {
92 s := standingOf(engine.ProofOfConcept(), player)
93 if s.MainClan != "" {
94 s.Rank = rankOf(s.Rating)
95 }
96 return s
97}
98
99// RatedPlayers counts the players on the ladder.
100func RatedPlayers() int {
101 return ladder.Size()
102}
103
104// ClanStandings lists every clan of the ruleset, highest rating first; equal ratings keep
105// ruleset order and share a rank.
106func ClanStandings() []ClanStanding {
107 rules := engine.ProofOfConcept()
108 list := []ClanStanding{}
109 for i, c := range decodeClans(clanRecords, len(rules.Clans)) {
110 list = append(list, ClanStanding{Clan: rules.Clans[i].Name, Rating: c.rating, Wins: c.wins, Losses: c.losses, Draws: c.draws})
111 }
112 for i := 1; i < len(list); i++ {
113 for j := i; j > 0 && list[j].Rating > list[j-1].Rating; j-- {
114 list[j], list[j-1] = list[j-1], list[j]
115 }
116 }
117 for i := range list {
118 if i > 0 && list[i].Rating == list[i-1].Rating {
119 list[i].Rank = list[i-1].Rank
120 } else {
121 list[i].Rank = i + 1
122 }
123 }
124 return list
125}
126
127// standingOf is player's standing without its rank.
128func standingOf(rules *engine.Ruleset, player address) Standing {
129 s := Standing{Player: player.String(), Name: nameOf(player), Rating: startRating}
130 st := loadStanding(player)
131 if st == nil {
132 return s
133 }
134 s.MainClan = clanName(rules, st.mainClan())
135 s.Rating, s.Wins, s.Losses, s.Draws = st.rating, st.wins, st.losses, st.draws
136 return s
137}
138
139// nameOf is player's gno.land name, "" when they have none.
140func nameOf(player address) string {
141 u := users.ResolveAddress(player)
142 if u.IsDeleted() {
143 return ""
144 }
145 return u.Name()
146}
147
148// Locks lists player's session locks. It can include locks whose session has expired.
149func Locks(player address) []SessionLock {
150 list := []SessionLock{}
151 for _, l := range loadPlayer(player).locks {
152 list = append(list, SessionLock{Session: l.session.String(), Match: strconv.Itoa(l.id), Expires: l.expires})
153 }
154 return list
155}
156
157// Matches lists player's matches, newest first: limit of them after skipping offset.
158func Matches(player address, offset, limit int) []MatchSummary {
159 checkPage(offset, limit)
160 rules := engine.ProofOfConcept()
161 now := time.Now().Unix()
162 list := []MatchSummary{}
163 for _, id := range loadPlayer(player).newest(offset, limit) {
164 m, ok := loadMatch(rules, id)
165 if !ok {
166 continue
167 }
168 list = append(list, m.summary(rules, id, now))
169 }
170 return list
171}
172
173// Match returns the view of match id.
174func Match(id string) MatchView {
175 rules := engine.ProofOfConcept()
176 n, m := find(rules, id)
177 return m.view(rules, n, time.Now().Unix())
178}
179
180func (m record) view(rules *engine.Ruleset, id int, now int64) MatchView {
181 v := MatchView{
182 MatchSummary: m.summary(rules, id, now),
183 ChallengerDeck: deckText(rules, m.decks[0]),
184 AcceptorDeck: deckText(rules, m.decks[1]),
185 Plans: [][]string{},
186 Box: base64.StdEncoding.EncodeToString([]byte(m.box)),
187 HP: [][]int{},
188 DeadlinePassed: m.running() && now > m.deadline,
189 }
190 resolved := plansMade(m.plans[0])
191 rounds := resolved
192 if m.running() {
193 rounds++
194 }
195 for r := 1; r <= rounds; r++ {
196 a := planText(rules, planAt(m.plans[0], r))
197 b := planText(rules, planAt(m.plans[1], r))
198 v.Plans = append(v.Plans, []string{a, b})
199 }
200 for r := 0; r < resolved; r++ {
201 v.HP = append(v.HP, []int{m.hp[r][0], m.hp[r][1]})
202 }
203 return v
204}
205
206func (m record) summary(rules *engine.Ruleset, id int, now int64) MatchSummary {
207 resolved := plansMade(m.plans[0])
208 s := MatchSummary{
209 ID: strconv.Itoa(id),
210 Phase: m.phase,
211 Round: resolved + 1,
212 Challenger: m.challenger.String(),
213 Acceptor: m.acceptor.String(),
214 ChallengerClan: clanName(rules, m.clans[0]),
215 AcceptorClan: clanName(rules, m.clans[1]),
216 Result: m.result,
217 Reason: m.reason,
218 Opponent: m.opponent.String(),
219 }
220 switch m.phase {
221 case phaseOpen:
222 s.Deadline = m.deadline
223 if m.expired(now) {
224 s.Phase = phaseExpired
225 }
226 case phaseReveal:
227 s.ToMove = s.Challenger
228 s.Deadline = m.deadline
229 case phasePlay:
230 s.ToMove = s.Acceptor
231 s.Deadline = m.deadline
232 case phaseEnded:
233 if resolved > 0 {
234 s.Round = resolved
235 }
236 }
237 return s
238}
239
240func (m record) running() bool {
241 return m.phase == phaseReveal || m.phase == phasePlay
242}
243
244// deckText names the cards of deck in card id order.
245func deckText(rules *engine.Ruleset, deck uint64) string {
246 var names []string
247 for i := range rules.Cards {
248 if deck&(uint64(1)<<uint(i)) != 0 {
249 names = append(names, rules.Cards[i].Name)
250 }
251 }
252 return strings.Join(names, ",")
253}
254
255// planText names the cards of p in play order.
256func planText(rules *engine.Ruleset, p engine.Plan) string {
257 names := make([]string, len(p))
258 for i, id := range p {
259 names[i] = rules.Cards[id].Name
260 }
261 return strings.Join(names, ",")
262}