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}