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

lobby.gno

5.69 Kb · 205 lines
  1package arena
  2
  3import (
  4	"chain"
  5
  6	// Only for OriginSend, in Enter. Caller identity always comes from
  7	// cur.Previous(), never from this package.
  8	"chain/runtime/unsafe"
  9)
 10
 11// arena is one game: its spec, the players who paid and wait, and once it
 12// started, its round. It is stored from the first paid entry and deleted
 13// when its round is closed or its lobby empties.
 14type arena struct {
 15	spec
 16	lobby   []address
 17	tickets []string // SHA-256 of each player's secret, 64 hex characters
 18	round   *round   // nil until the game starts
 19}
 20
 21func getArena(id string) *arena {
 22	v := arenas.Get(id)
 23	if v == nil {
 24		return nil
 25	}
 26	return v.(*arena)
 27}
 28
 29func mustArena(id string) *arena {
 30	a := getArena(id)
 31	if a == nil {
 32		panic(errUnknownArena)
 33	}
 34	return a
 35}
 36
 37// Enter puts the caller in the lobby of the arena that spec describes, and
 38// returns its id. The exact stake must be attached to the transaction
 39// (-send "<stake>ugnot") and stays in the realm until the player leaves or
 40// the game is over.
 41//
 42// The first entry creates the arena, the next ones join it: the realm
 43// recomputes the id from the spec every time. An arena takes entries for an
 44// hour after it was created, and until its game starts.
 45//
 46// ticket is 64 lowercase hex characters, the SHA-256 of a secret the
 47// player's client made up: the game server gives the seat to whoever shows
 48// the secret.
 49//
 50// It must be called directly by a user (maketx call): a call relayed by
 51// another realm or by a maketx run script is refused.
 52func Enter(cur realm, spec string, ticket string) string {
 53	player := callerOf(0, cur)
 54	// Paid-call guard: two checks that are only correct together.
 55	//
 56	// (a) The caller is a plain user account. Only then is the -send
 57	//     envelope guaranteed to have landed at this realm's address. A
 58	//     relaying realm or a maketx run script can attach coins to the
 59	//     transaction and keep them, while OriginSend still reports them.
 60	if !cur.Previous().IsUserCall() {
 61		panic(errNotUserCall)
 62	}
 63	sp := parseSpec(spec)
 64	id := idOf(spec)
 65	t := now()
 66	if t > sp.created+entryWindow {
 67		panic(errEntryClosed)
 68	}
 69	if sp.created > t+clockSkew {
 70		panic(errNotYetCreated)
 71	}
 72	if !validTicket(ticket) {
 73		panic(errInvalidTicket)
 74	}
 75	a := getArena(id)
 76	if a == nil {
 77		a = &arena{spec: sp}
 78	}
 79	if a.round != nil {
 80		panic(errRoundRunning)
 81	}
 82	if indexOfAddr(a.lobby, player) >= 0 {
 83		panic(errAlreadyInLobby)
 84	}
 85	if int64(len(a.lobby)) >= a.maxSeats {
 86		panic(errLobbyFull)
 87	}
 88	// (b) The envelope is exactly the stake: one coin, the right denom, the
 89	//     right amount. Anything else would either underpay or leave coins
 90	//     in the realm that it owes to nobody.
 91	sent := unsafe.OriginSend()
 92	if len(sent) != 1 || sent[0].Denom != payDenom || sent[0].Amount != a.stake {
 93		panic("gnofly: wrong payment, send exactly " + itoa(a.stake) + payDenom)
 94	}
 95
 96	// The coins are already at the realm's address: the chain moved them
 97	// before the call. All that is left is to write down who they belong to.
 98	a.lobby = withAddr(a.lobby, player)
 99	a.tickets = withString(a.tickets, ticket)
100	arenas.Set(id, a)
101	hold(a.stake)
102	// The guard above says the coins arrived. This looks at the balance.
103	assertSolvent(0, cur)
104
105	chain.Emit("Entered",
106		"arena", id,
107		"player", player.String(),
108		"stake", itoa(a.stake),
109		"ticket", ticket,
110		"lobby", itoa(int64(len(a.lobby))),
111	)
112	return id
113}
114
115// Leave takes the caller out of the lobby of an arena and sends their stake
116// back. It works until the game starts. The last player out deletes the
117// arena: nothing is left on chain of a game nobody played.
118func Leave(cur realm, id string) {
119	player := callerOf(0, cur)
120	a := mustArena(id)
121	seat := indexOfAddr(a.lobby, player)
122	if seat < 0 {
123		panic(errNotInLobby)
124	}
125	a.lobby = withoutAddr(a.lobby, seat)
126	a.tickets = withoutString(a.tickets, seat)
127	if len(a.lobby) == 0 {
128		arenas.Remove(id)
129	}
130	release(a.stake)
131
132	pay(0, cur, player, a.stake)
133	assertSolvent(0, cur)
134
135	chain.Emit("Left",
136		"arena", id,
137		"player", player.String(),
138		"stake", itoa(a.stake),
139	)
140}
141
142// ExpireLobby closes an arena whose game never started: every player in the
143// lobby gets their stake back, and the arena is deleted. Anyone can call it
144// once the arena stopped taking entries, an hour after it was created.
145func ExpireLobby(cur realm, id string) {
146	caller := callerOf(0, cur)
147	a := mustArena(id)
148	if a.round != nil {
149		panic(errRoundRunning)
150	}
151	if now() <= a.created+entryWindow {
152		panic(errNotExpired)
153	}
154	players := a.lobby
155	arenas.Remove(id)
156	release(a.stake * int64(len(players)))
157	for _, p := range players {
158		pay(0, cur, p, a.stake)
159	}
160	assertSolvent(0, cur)
161
162	chain.Emit("LobbyExpired",
163		"arena", id,
164		"players", itoa(int64(len(players))),
165		"caller", caller.String(),
166	)
167}
168
169// The four helpers below return a new slice of exactly the length of its
170// content, so that nothing else stays in storage. They return nil for an
171// empty list.
172
173// withAddr returns list with v added at the end.
174func withAddr(list []address, v address) []address {
175	out := make([]address, len(list), len(list)+1)
176	copy(out, list)
177	return append(out, v)
178}
179
180// withString returns list with v added at the end.
181func withString(list []string, v string) []string {
182	out := make([]string, len(list), len(list)+1)
183	copy(out, list)
184	return append(out, v)
185}
186
187// withoutAddr returns list without the element at index at.
188func withoutAddr(list []address, at int) []address {
189	if len(list) == 1 {
190		return nil
191	}
192	out := make([]address, 0, len(list)-1)
193	out = append(out, list[:at]...)
194	return append(out, list[at+1:]...)
195}
196
197// withoutString returns list without the element at index at.
198func withoutString(list []string, at int) []string {
199	if len(list) == 1 {
200		return nil
201	}
202	out := make([]string, 0, len(list)-1)
203	out = append(out, list[:at]...)
204	return append(out, list[at+1:]...)
205}