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}