rounds.gno
9.08 Kb · 316 lines
1package arena
2
3import (
4 "chain"
5 "strings"
6
7 "gno.land/p/g1t2kg2vtr3fukg43eujkn6x53gfdyakhngt4sfd/gnofly/arena/payout/v0"
8)
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.
12type 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.
25func 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.
59func 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.
87func 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.
142func 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.
171func 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.
200func 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.
213func 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.
224func 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.
260func 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.
269const (
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.
276func 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.
287func RefundDelay() int64 {
288 return refundDelay
289}
290
291// joinAddrs writes addresses separated by commas, for an event.
292func 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.
305func 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}