Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}