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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}