lobby.gno
5.75 Kb ยท 245 lines
1package chess
2
3import (
4 "std"
5 "time"
6
7 "gno.land/p/demo/avl"
8)
9
10type lobbyPlayer struct {
11 joinedAt time.Time
12 seenAt time.Time
13 player *Player
14}
15
16func (l lobbyPlayer) r(cat Category) float64 { return l.player.CategoryInfo[cat].Rating }
17
18// returns whether the rating r is within l's current "acceptable" range.
19func (l lobbyPlayer) acceptRating(cat Category, r float64) bool {
20 delta := int(time.Since(l.joinedAt) / time.Second)
21 if delta >= 10 {
22 return true
23 }
24
25 halfsize := bracketSize[delta] / 2
26 rat := l.r(cat)
27 return r >= (rat-halfsize) && r <= (rat+halfsize)
28}
29
30type tcLobby byte
31
32// time control lobby divisions. N+M -> tcLobbyNpM
33const (
34 tcLobby5p0 tcLobby = iota
35 tcLobby10p5
36 tcLobbyMax
37)
38
39func (tc tcLobby) Category() Category {
40 switch tc {
41 case tcLobby5p0:
42 return Blitz
43 default:
44 return Rapid
45 }
46}
47
48func (tc tcLobby) Time() (secs, incr int) {
49 switch tc {
50 case tcLobby5p0:
51 return 60 * 5, 0
52 case tcLobby10p5:
53 return 60 * 10, 5
54 default:
55 panic("invalid tc value")
56 }
57}
58
59var (
60 lobby [tcLobbyMax][]lobbyPlayer
61 lobbyPlayer2Game avl.Tree // player addr -> *Game. set after a user is matched; reset when joining again.
62)
63
64func LobbyJoin(cur realm, seconds, increment int) {
65 assertOriginCall()
66 var tc tcLobby
67 switch {
68 case seconds == (60*5) && increment == 0:
69 tc = tcLobby5p0
70 case seconds == (60*10) && increment == 5:
71 tc = tcLobby10p5
72 default:
73 panic("can only use time controls 5+0 or 10+5")
74 }
75
76 // Ensure that user's previous games are finished (or timed out).
77 caller := std.PreviousRealm().Address()
78
79 games := getUserGames(caller)
80 // XXX: temporary to avoid ever prohibiting a user from joining the lobby.
81 // when possible, change back to assertGamesFinished(games)
82 for _, g := range games {
83 if !g.State.IsFinished() {
84 if err := resign(g); err != nil {
85 panic("internal error (could not resign game " + g.ID + "): " + err.Error())
86 }
87 }
88 }
89 assertUserNotInLobby(caller)
90
91 // remove caller from lobbyPlayer2Game, so LobbyGameFound
92 // returns the right value.
93 lobbyPlayer2Game.Remove(caller.String())
94
95 now := time.Now()
96 lobby[tc] = append(lobby[tc], lobbyPlayer{joinedAt: now, seenAt: now, player: getPlayer(caller)})
97 refreshLobby(tc)
98}
99
100func assertUserNotInLobby(caller std.Address) {
101 for _, sublob := range lobby {
102 for _, pl := range sublob {
103 if pl.player.Address == caller {
104 panic("you are already in the lobby")
105 }
106 }
107 }
108}
109
110// refreshLobby serves to run through the lobby, kick timed out users, and see if any users
111// can be matched with the current user.
112func refreshLobby(tc tcLobby) {
113 callerAddr := std.PreviousRealm().Address()
114 now := time.Now()
115 for idx, player := range lobby[tc] {
116 if player.player.Address == callerAddr {
117 // mark player as seen now
118 lobby[tc][idx].seenAt = now
119 break
120 }
121 }
122
123 // lobby housekeeping: kick any player that hasn't contacted us for the
124 // past 30 seconds.
125 // do this BEFORE matching the caller, as we want to give them someone who
126 // is seemingly active in the lobby.
127 for i := 0; i < len(lobby[tc]); i++ {
128 if now.Sub(lobby[tc][i].seenAt) >= time.Second*30 {
129 newLobby := append([]lobbyPlayer{}, lobby[tc][:i]...)
130 lobby[tc] = append(newLobby, lobby[tc][i+1:]...)
131 i--
132 }
133 }
134
135 // determine sub lobby
136 sublob := lobby[tc]
137
138 callerPos := -1
139 var caller lobbyPlayer
140 for idx, player := range sublob {
141 if player.player.Address == callerAddr {
142 callerPos = idx
143 caller = player
144 break
145 }
146 }
147 // caller is not involved in lobby, or lobby only contains the player
148 if callerPos < 0 || len(sublob) < 2 {
149 return
150 }
151
152 cat := tc.Category()
153 callerRating := caller.r(cat)
154 callerForce := now.Sub(caller.joinedAt) >= time.Second*10
155
156 for i, player := range sublob {
157 if i == callerPos {
158 continue
159 }
160 // force if either the caller or the player have been waiting for more than 10s.
161 force := callerForce || (now.Sub(player.joinedAt) >= time.Second*10)
162 // find player whose rating falls in each other's range.
163 if force || (caller.acceptRating(cat, player.r(cat)) &&
164 player.acceptRating(cat, callerRating)) {
165 lobbyMatch(tc, callerPos, i)
166 return
167 }
168 }
169}
170
171func lobbyMatch(tc tcLobby, p1, p2 int) {
172 // Get the two players, create a new game with them.
173 secs, incr := tc.Time()
174 a1, a2 := lobby[tc][p1].player.Address, lobby[tc][p2].player.Address
175
176 game := newGame(a1, a2, secs, incr)
177
178 // remove p1 and p2 from lobby
179 if p1 > p2 {
180 p1, p2 = p2, p1
181 }
182 nl := append([]lobbyPlayer{}, lobby[tc][:p1]...)
183 nl = append(nl, lobby[tc][p1+1:p2]...)
184 nl = append(nl, lobby[tc][p2+1:]...)
185 lobby[tc] = nl
186
187 // add to lobbyPlayer2Game
188 lobbyPlayer2Game.Set(a1.String(), game)
189 lobbyPlayer2Game.Set(a2.String(), game)
190}
191
192/*
193generated by python code:
194
195 for i in range(0,10):
196 print((i+1)**(3.694692926)+49, ',')
197
198rationale: give brackets in an exponential range between
19950 and 5000, dividing it into 10 steps.
200"magic constant" obtained solving for in c in the equation:
201
202 5000=(x+1)^c+49 (x = steps, 10 in our case)
203
204which comes out to be ln(delta)/ln(steps), delta = 5000-49, steps = 10.
205*/
206var bracketSize = [...]float64{
207 50.0,
208 61.9483191543645,
209 106.91839582826664,
210 216.65896892328266,
211 431.3662312611604,
212 798.94587409321,
213 1374.4939512498888,
214 2219.9018387103433,
215 3403.5405753197747,
216 5000,
217}
218
219func LobbyGameFound(cur realm) string {
220 refreshLobby(tcLobby5p0)
221 refreshLobby(tcLobby10p5)
222
223 val, ok := lobbyPlayer2Game.Get(std.PreviousRealm().Address().String())
224 if !ok {
225 return "null"
226 }
227 return val.(*Game).json()
228}
229
230func LobbyQuit(cur realm) {
231 caller := std.PreviousRealm().Address()
232 for tc, sublob := range lobby {
233 for i, pl := range sublob {
234 if pl.player.Address == caller {
235 newLobby := append([]lobbyPlayer{}, sublob[:i]...)
236 newLobby = append(newLobby, sublob[i+1:]...)
237 lobby[tc] = newLobby
238 lobbyPlayer2Game.Remove(caller.String())
239 return
240 }
241 }
242 }
243
244 panic("you are not in the lobby")
245}