gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/v0⧉ Realm Open in gnoweb ↗
Kind Realm (renderable)
Name v0
Namespace g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd / gnofly / arena
Exported functions 21
Module gno.land/r/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/v0
gno 0.9 rounds.gno gno
⧉
1 package arena 2 3 import ( 4 "chain" 5 "strings" 6 7 "gno.land/p/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/payout/v0" 8 ArenaJSON (id string) string
ArenasJSON () string
CancelRound (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string)
Claim (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
Claimable (addr string) int64
ClaimsJSON (addr string) string
EarningsJSON () string
Enter (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, spec string, ticket string) string
Escrowed () int64
ExpireLobby (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string)
Leave (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string)
PlayerJSON (addr string) string
RefundDelay () int64
RefundRound (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string)
Render (path string) string
ResultJSON (id string) string
ResultsJSON () string
SetRefundDelay (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, seconds int64)
SettleRound (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string, deaths string)
StartRound (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id string) int64
Version () int64
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
)
9
10 // round is the started game of an arena. Its players are fixed at start, and
11 // each of them has one stake in it.
12 type round struct {
13 startedAt int64 // block time, Unix seconds
14 players []address // in the order they entered
15 }
16
17 // A game ends in exactly one of three ways: SettleRound, CancelRound or
18 // RefundRound. Each of them deletes the arena before anything else, so a
19 // second one finds no arena and aborts.
20
21 // StartRound starts the game of an arena with every player of its lobby.
22 // Referee only. The lobby must hold at least minSeats players; it holds at
23 // most maxSeats, Enter sees to that. No coin moves: the stakes that were
24 // owed to the lobby are now owed to the round. It returns how many play.
25 func StartRound(cur realm, id string ) int64 {
26 referee := assertReferee( 0 , cur)
27 a := mustArena(id)
28 if a.round != nil {
29 panic(errRoundRunning)
30 }
31 if int64 (len(a.lobby)) < a.minSeats {
32 panic(errTooFewPlayers)
33 }
34 a.round = &round{startedAt: now(), players: a.lobby}
35 a.lobby = nil
36 a.tickets = nil
37
38 chain.Emit( "RoundStarted" ,
39 "arena" , id,
40 "players" , joinAddrs(a.round.players),
41 "startedAt" , itoa(a.round.startedAt),
42 "referee" , referee.String(),
43 )
44 return int64 (len(a.round.players))
45 }
46
47 // SettleRound closes the game of an arena: it applies the rule of its mode
48 // to what happened, pays the players, and pays the fee to the treasury.
49 // Referee only.
50 //
51 // deaths is "victim:killer" entries separated by ";", in the order they
52 // happened, "victim:" when nobody shot the victim down, and the empty
53 // string when nobody died. A player dies at most as often as the arena has
54 // lives.
55 //
56 // The referee is trusted for these facts and for nothing else. The rule
57 // refuses a victim or a killer who is not a player of the round, and decides
58 // the amounts. The realm then checks them (see settle) before it pays.
59 func SettleRound(cur realm, id string , deaths string ) {
60 referee := assertReferee( 0 , cur)
61 a := runningArena(id)
62 list, err := payout.ParseDeaths(deaths)
63 if err != nil {
64 panic(err)
65 }
66 rule := payout.Lookup(a.mode)
67 if rule == nil {
68 panic(errInvalidMode) // cannot happen: parseSpec checked the mode
69 }
70 // The rule gets a copy of the players: the list the realm pays from is
71 // not one a rule can touch.
72 players := make([]address, len(a.round.players))
73 copy(players, a.round.players)
74 g := payout.Game{Stake: a.stake, Lives: a.lives, KillTarget: a.killTarget, FeeBps: feeBps}
75 res, err := rule.Settle(g, players, list)
76 if err != nil {
77 panic(err)
78 }
79 settle( 0 , cur, id, a, res, referee)
80 }
81
82 // settle closes the game of an arena with the result of its rule.
83 //
84 // It trusts nothing about the result. Before a counter or a coin moves,
85 // checkResult verifies that only players of the round are paid and that the
86 // payouts and the fee add up to the pot, to the ugnot.
87 func settle(_ int , rlm realm, id string , a *arena, res payout.Result, referee address) {
88 r := a.round
89 pot := a.stake * int64 (len(r.players))
90 checkResult(pot, r.players, res)
91
92 // The fee goes to the treasury of the game realm as it is now. If that
93 // is not an address the realm can pay, the fee goes back to the players
94 // instead: shared by the order of the round, the remainder to the first.
95 fee := res.Fee
96 payouts := res.Payouts
97 treasury := roles.Treasury()
98 if fee > 0 && (!validAddress(treasury) || treasury == rlm.Address()) {
99 payouts = withFeeShared(r.players, payouts, fee)
100 fee = 0
101 }
102
103 // State first: the arena is gone and its pot is off the books before the
104 // first coin leaves.
105 arenas.Remove(id)
106 release(pot)
107 record(id, result{
108 mode: a.mode,
109 stake: a.stake,
110 outcome: outcomeSettled,
111 players: int64 (len(r.players)),
112 pot: pot,
113 fee: fee,
114 closedAt: now(),
115 payouts: payouts,
116 })
117 creditRound(a.stake, r.players, payouts)
118
119 // The winnings stay in the realm until each player claims them: a
120 // settlement sends no coin to a player, so it cannot fail on one.
121 for _, p := range payouts {
122 credit(p.Address, id, p.Amount)
123 }
124 if fee > 0 {
125 pay( 0 , rlm, treasury, fee)
126 }
127 assertSolvent( 0 , rlm)
128
129 chain.Emit( "RoundSettled" ,
130 "arena" , id,
131 "players" , itoa( int64 (len(r.players))),
132 "pot" , itoa(pot),
133 "fee" , itoa(fee),
134 "payouts" , joinPayouts(payouts),
135 "treasury" , treasury.String(),
136 "referee" , referee.String(),
137 )
138 }
139
140 // withFeeShared returns payouts with fee added, shared between every player
141 // of the round, in their order: the remainder goes to the first.
142 func withFeeShared(players []address, payouts []payout.Payout, fee int64 ) []payout.Payout {
143 each := fee / int64 (len(players))
144 extra := fee - each* int64 (len(players))
145 out := make([]payout.Payout, 0 , len(players))
146 for i, p := range players {
147 amount := each
148 if i == 0 {
149 amount += extra
150 }
151 for _, q := range payouts {
152 if q.Address == p {
153 amount += q.Amount
154 }
155 }
156 if amount > 0 {
157 out = append(out, payout.Payout{Address: p, Amount: amount})
158 }
159 }
160 return out
161 }
162
163 // checkResult panics unless a result can be paid out of a pot: every payout
164 // goes to a player of the round, a player is paid at most once and never
165 // zero or less, the fee is not negative, and all of it adds up to the pot
166 // exactly.
167 //
168 // The built-in rules guarantee all of this. The check is here so that the
169 // realm does not depend on it: whatever a rule returns, the realm never
170 // sends more than the pot, and never to a stranger.
171 func checkResult(pot int64 , players []address, res payout.Result) {
172 // Every amount is bounded by the pot before it is added, so the sum
173 // cannot overflow: at most 9 terms of at most 8e9.
174 if res.Fee < 0 || res.Fee > pot {
175 panic(errNotConserved)
176 }
177 sum := res.Fee
178 paid := make([] bool , len(players))
179 for _, p := range res.Payouts {
180 seat := indexOfAddr(players, p.Address)
181 if seat < 0 || paid[seat] || p.Amount <= 0 {
182 panic(errBadSettlement)
183 }
184 if p.Amount > pot {
185 panic(errNotConserved)
186 }
187 paid[seat] = true
188 sum += p.Amount
189 }
190 if sum != pot {
191 panic(errNotConserved)
192 }
193 }
194
195 // CancelRound closes the game of an arena without a result: every player
196 // gets their stake back in full. Referee only.
197 //
198 // The game server uses it for a game it cannot referee, for example one it
199 // finds running when it starts.
200 func CancelRound(cur realm, id string ) {
201 referee := assertReferee( 0 , cur)
202 a := runningArena(id)
203 refund( 0 , cur, id, a, outcomeCancelled, "RoundCancelled" , referee)
204 }
205
206 // RefundRound closes the game of an arena without a result, like
207 // CancelRound, and can be called by anyone, once
208 //
209 // startedAt + seconds + RefundDelay()
210 //
211 // has passed on the block clock. It is the players' way out when the
212 // referee never settles: no stake depends on a server staying up.
213 func RefundRound(cur realm, id string ) {
214 caller := callerOf( 0 , cur)
215 a := runningArena(id)
216 if now() < a.round.startedAt+a.seconds+refundDelay {
217 panic(errRefundTooEarly)
218 }
219 refund( 0 , cur, id, a, outcomeRefunded, "RoundRefunded" , caller)
220 }
221
222 // refund closes the game of an arena and sends every player their stake
223 // back. No fee is taken.
224 func refund(_ int , rlm realm, id string , a *arena, outcome, event string , caller address) {
225 r := a.round
226 pot := a.stake * int64 (len(r.players))
227 payouts := make([]payout.Payout, len(r.players))
228 for i, p := range r.players {
229 payouts[i] = payout.Payout{Address: p, Amount: a.stake}
230 }
231
232 // State first, as in settle.
233 arenas.Remove(id)
234 release(pot)
235 record(id, result{
236 mode: a.mode,
237 stake: a.stake,
238 outcome: outcome,
239 players: int64 (len(r.players)),
240 pot: pot,
241 closedAt: now(),
242 payouts: payouts,
243 })
244
245 for _, p := range r.players {
246 pay( 0 , rlm, p, a.stake)
247 }
248 assertSolvent( 0 , rlm)
249
250 chain.Emit(event,
251 "arena" , id,
252 "players" , itoa( int64 (len(r.players))),
253 "pot" , itoa(pot),
254 "caller" , caller.String(),
255 )
256 }
257
258 // runningArena returns an arena whose game is running. It panics for an
259 // unknown arena and for one whose game has not started.
260 func runningArena(id string ) *arena {
261 a := mustArena(id)
262 if a.round == nil {
263 panic(errNoRound)
264 }
265 return a
266 }
267
268 // Refund delay bounds, in seconds.
269 const (
270 minRefundDelay = 60
271 maxRefundDelay = 86400
272 )
273
274 // SetRefundDelay sets how long after a game's time limit anyone may refund
275 // it. Admin only.
276 func SetRefundDelay(cur realm, seconds int64 ) {
277 assertAdmin( 0 , cur)
278 if seconds < minRefundDelay || seconds > maxRefundDelay {
279 panic(errInvalidRefundDelay)
280 }
281 refundDelay = seconds
282 version++
283 chain.Emit( "RefundDelaySet" , "seconds" , itoa(seconds))
284 }
285
286 // RefundDelay returns the refund delay, in seconds.
287 func RefundDelay() int64 {
288 return refundDelay
289 }
290
291 // joinAddrs writes addresses separated by commas, for an event.
292 func joinAddrs(list []address) string {
293 var b strings.Builder
294 for i, a := range list {
295 if i > 0 {
296 b.WriteByte( ',' )
297 }
298 b.WriteString(a.String())
299 }
300 return b.String()
301 }
302
303 // joinPayouts writes payouts as "address:amount" separated by commas, for
304 // an event and for the stored results.
305 func joinPayouts(list []payout.Payout) string {
306 var b strings.Builder
307 for i, p := range list {
308 if i > 0 {
309 b.WriteByte( ',' )
310 }
311 b.WriteString(p.Address.String())
312 b.WriteByte( ':' )
313 b.WriteString(itoa(p.Amount))
314 }
315 return b.String()
316 }
317
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.