json.gno
6.01 Kb · 201 lines
1package arena
2
3import "strings"
4
5// The JSON documents are built by hand. That is safe because every string
6// that reaches them was validated against a JSON-safe charset when it was
7// written: ids are hex, modes and nonces [a-z0-9-], tickets hex, addresses
8// bech32. The one string that is not under the realm's control, an address
9// typed into a query, goes through quote.
10//
11// Ordering is deterministic everywhere: arenas follow their id (the tree
12// iterates in key order), a lobby and a round the order of entry, results
13// are newest first and earnings best first.
14
15// ArenasJSON returns every arena someone paid into and whose game is not
16// over, as a JSON array sorted by id:
17//
18// [
19// { "id": "3f2a...", "mode": "deathmatch", "stake": 10000000,
20// "minSeats": 2, "maxSeats": 8, "lives": 3, "seconds": 900, "killTarget": 5,
21// "creator": "g1...", "created": 1790950000, "nonce": "k3x9",
22// "lobby": [ { "address": "g1...", "ticket": "9f86d0...00a08" } ],
23// "round": { "startedAt": 1790950300, "players": [ "g1...", "g1..." ] } }
24// ]
25//
26// The output has no whitespace. "round" is null until the game starts.
27// The fields from "mode" to "nonce" are the arena's spec: the game server
28// rebuilds the spec string, and so the id, from them.
29func ArenasJSON() string {
30 var b strings.Builder
31 b.WriteByte('[')
32 first := true
33 arenas.Iterate("", "", func(id string, v any) bool {
34 if !first {
35 b.WriteByte(',')
36 }
37 first = false
38 writeArena(&b, id, v.(*arena))
39 return false
40 })
41 b.WriteByte(']')
42 return b.String()
43}
44
45// ArenaJSON returns one arena in the shape of ArenasJSON, or null.
46func ArenaJSON(id string) string {
47 a := getArena(id)
48 if a == nil {
49 return "null"
50 }
51 var b strings.Builder
52 writeArena(&b, id, a)
53 return b.String()
54}
55
56func writeArena(b *strings.Builder, id string, a *arena) {
57 b.WriteString(`{"id":"` + id + `","mode":"` + a.mode + `"`)
58 for _, f := range []struct {
59 k string
60 v int64
61 }{{"stake", a.stake}, {"minSeats", a.minSeats}, {"maxSeats", a.maxSeats}, {"lives", a.lives},
62 {"seconds", a.seconds}, {"killTarget", a.killTarget}} {
63 b.WriteString(`,"` + f.k + `":` + itoa(f.v))
64 }
65 b.WriteString(`,"creator":"` + a.creator.String() + `","created":` + itoa(a.created) + `,"nonce":"` + a.nonce + `"`)
66 b.WriteString(`,"lobby":[`)
67 for i, p := range a.lobby {
68 if i > 0 {
69 b.WriteByte(',')
70 }
71 b.WriteString(`{"address":"` + p.String() + `","ticket":"` + a.tickets[i] + `"}`)
72 }
73 b.WriteString(`],"round":`)
74 if a.round == nil {
75 b.WriteString("null}")
76 return
77 }
78 b.WriteString(`{"startedAt":` + itoa(a.round.startedAt) + `,"players":[`)
79 for i, p := range a.round.players {
80 if i > 0 {
81 b.WriteByte(',')
82 }
83 b.WriteString(`"` + p.String() + `"`)
84 }
85 b.WriteString("]}}")
86}
87
88// ResultsJSON returns the last closed games, newest first, at most 50:
89//
90// [ { "id": "3f2a...", "mode": "deathmatch", "stake": 10000000,
91// "outcome": "settled", "players": 2, "pot": 20000000, "fee": 500000,
92// "closedAt": 1790951000,
93// "payouts": [ { "address": "g1...", "amount": 19500000 } ] } ]
94func ResultsJSON() string {
95 var b strings.Builder
96 b.WriteByte('[')
97 for i := len(closed) - 1; i >= 0; i-- {
98 if i < len(closed)-1 {
99 b.WriteByte(',')
100 }
101 r, _ := resultOf(closed[i])
102 writeResult(&b, closed[i], r)
103 }
104 b.WriteByte(']')
105 return b.String()
106}
107
108// ResultJSON returns the result of one closed game in the shape of
109// ResultsJSON, or null when the realm does not have it (still running, or
110// too old).
111func ResultJSON(id string) string {
112 r, ok := resultOf(id)
113 if !ok {
114 return "null"
115 }
116 var b strings.Builder
117 writeResult(&b, id, r)
118 return b.String()
119}
120
121func writeResult(b *strings.Builder, id string, r result) {
122 b.WriteString(`{"id":"` + id + `","mode":"` + r.mode + `","stake":` + itoa(r.stake) +
123 `,"outcome":"` + r.outcome + `","players":` + itoa(r.players) + `,"pot":` + itoa(r.pot) +
124 `,"fee":` + itoa(r.fee) + `,"closedAt":` + itoa(r.closedAt) + `,"payouts":[`)
125 for i, p := range r.payouts {
126 if i > 0 {
127 b.WriteByte(',')
128 }
129 b.WriteString(`{"address":"` + p.Address.String() + `","amount":` + itoa(p.Amount) + `}`)
130 }
131 b.WriteString("]}")
132}
133
134// EarningsJSON returns the top winners as a JSON array, best first, at most
135// 10 entries:
136//
137// [ { "address": "g1...", "won": 19000000, "rounds": 3 } ]
138//
139// The output has no whitespace. "won" adds up, round after round, what the
140// player was paid above their own stake, never less than zero for a round.
141// "rounds" is how many settled rounds they played. With equal winnings, the
142// player who got there first stays ahead.
143func EarningsJSON() string {
144 var b strings.Builder
145 b.WriteByte('[')
146 for i, addr := range winners.addrs {
147 if i > 0 {
148 b.WriteByte(',')
149 }
150 writeTotals(&b, `"`+addr.String()+`"`, totalsOf(addr))
151 }
152 b.WriteByte(']')
153 return b.String()
154}
155
156// PlayerJSON returns one player's totals as a JSON object:
157//
158// { "address": "g1...", "won": 19000000, "rounds": 3 }
159//
160// The output has no whitespace. An unknown address returns the same shape
161// with zero won and zero rounds.
162func PlayerJSON(addr address) string {
163 var b strings.Builder
164 writeTotals(&b, quote(addr.String()), totalsOf(addr))
165 return b.String()
166}
167
168// writeTotals writes the totals of a player. quoted is the player's address
169// as a JSON string literal, quotes included.
170func writeTotals(b *strings.Builder, quoted string, t totals) {
171 b.WriteString(`{"address":`)
172 b.WriteString(quoted)
173 b.WriteString(`,"won":`)
174 b.WriteString(itoa(t.won))
175 b.WriteString(`,"rounds":`)
176 b.WriteString(itoa(t.rounds))
177 b.WriteByte('}')
178}
179
180// quote returns s as a JSON string literal, quotes included. It is for the
181// strings the realm did not validate. Quotes and backslashes are escaped and
182// anything outside printable ASCII is replaced by "?", so the result is
183// valid JSON whatever s holds.
184func quote(s string) string {
185 var b strings.Builder
186 b.WriteByte('"')
187 for i := 0; i < len(s); i++ {
188 c := s[i]
189 switch {
190 case c == '"' || c == '\\':
191 b.WriteByte('\\')
192 b.WriteByte(c)
193 case c < 0x20 || c > 0x7e:
194 b.WriteByte('?')
195 default:
196 b.WriteByte(c)
197 }
198 }
199 b.WriteByte('"')
200 return b.String()
201}