package chess import ( "std" "time" "gno.land/p/demo/avl" ) type lobbyPlayer struct { joinedAt time.Time seenAt time.Time player *Player } func (l lobbyPlayer) r(cat Category) float64 { return l.player.CategoryInfo[cat].Rating } // returns whether the rating r is within l's current "acceptable" range. func (l lobbyPlayer) acceptRating(cat Category, r float64) bool { delta := int(time.Since(l.joinedAt) / time.Second) if delta >= 10 { return true } halfsize := bracketSize[delta] / 2 rat := l.r(cat) return r >= (rat-halfsize) && r <= (rat+halfsize) } type tcLobby byte // time control lobby divisions. N+M -> tcLobbyNpM const ( tcLobby5p0 tcLobby = iota tcLobby10p5 tcLobbyMax ) func (tc tcLobby) Category() Category { switch tc { case tcLobby5p0: return Blitz default: return Rapid } } func (tc tcLobby) Time() (secs, incr int) { switch tc { case tcLobby5p0: return 60 * 5, 0 case tcLobby10p5: return 60 * 10, 5 default: panic("invalid tc value") } } var ( lobby [tcLobbyMax][]lobbyPlayer lobbyPlayer2Game avl.Tree // player addr -> *Game. set after a user is matched; reset when joining again. ) func LobbyJoin(cur realm, seconds, increment int) { assertOriginCall() var tc tcLobby switch { case seconds == (60*5) && increment == 0: tc = tcLobby5p0 case seconds == (60*10) && increment == 5: tc = tcLobby10p5 default: panic("can only use time controls 5+0 or 10+5") } // Ensure that user's previous games are finished (or timed out). caller := std.PreviousRealm().Address() games := getUserGames(caller) // XXX: temporary to avoid ever prohibiting a user from joining the lobby. // when possible, change back to assertGamesFinished(games) for _, g := range games { if !g.State.IsFinished() { if err := resign(g); err != nil { panic("internal error (could not resign game " + g.ID + "): " + err.Error()) } } } assertUserNotInLobby(caller) // remove caller from lobbyPlayer2Game, so LobbyGameFound // returns the right value. lobbyPlayer2Game.Remove(caller.String()) now := time.Now() lobby[tc] = append(lobby[tc], lobbyPlayer{joinedAt: now, seenAt: now, player: getPlayer(caller)}) refreshLobby(tc) } func assertUserNotInLobby(caller std.Address) { for _, sublob := range lobby { for _, pl := range sublob { if pl.player.Address == caller { panic("you are already in the lobby") } } } } // refreshLobby serves to run through the lobby, kick timed out users, and see if any users // can be matched with the current user. func refreshLobby(tc tcLobby) { callerAddr := std.PreviousRealm().Address() now := time.Now() for idx, player := range lobby[tc] { if player.player.Address == callerAddr { // mark player as seen now lobby[tc][idx].seenAt = now break } } // lobby housekeeping: kick any player that hasn't contacted us for the // past 30 seconds. // do this BEFORE matching the caller, as we want to give them someone who // is seemingly active in the lobby. for i := 0; i < len(lobby[tc]); i++ { if now.Sub(lobby[tc][i].seenAt) >= time.Second*30 { newLobby := append([]lobbyPlayer{}, lobby[tc][:i]...) lobby[tc] = append(newLobby, lobby[tc][i+1:]...) i-- } } // determine sub lobby sublob := lobby[tc] callerPos := -1 var caller lobbyPlayer for idx, player := range sublob { if player.player.Address == callerAddr { callerPos = idx caller = player break } } // caller is not involved in lobby, or lobby only contains the player if callerPos < 0 || len(sublob) < 2 { return } cat := tc.Category() callerRating := caller.r(cat) callerForce := now.Sub(caller.joinedAt) >= time.Second*10 for i, player := range sublob { if i == callerPos { continue } // force if either the caller or the player have been waiting for more than 10s. force := callerForce || (now.Sub(player.joinedAt) >= time.Second*10) // find player whose rating falls in each other's range. if force || (caller.acceptRating(cat, player.r(cat)) && player.acceptRating(cat, callerRating)) { lobbyMatch(tc, callerPos, i) return } } } func lobbyMatch(tc tcLobby, p1, p2 int) { // Get the two players, create a new game with them. secs, incr := tc.Time() a1, a2 := lobby[tc][p1].player.Address, lobby[tc][p2].player.Address game := newGame(a1, a2, secs, incr) // remove p1 and p2 from lobby if p1 > p2 { p1, p2 = p2, p1 } nl := append([]lobbyPlayer{}, lobby[tc][:p1]...) nl = append(nl, lobby[tc][p1+1:p2]...) nl = append(nl, lobby[tc][p2+1:]...) lobby[tc] = nl // add to lobbyPlayer2Game lobbyPlayer2Game.Set(a1.String(), game) lobbyPlayer2Game.Set(a2.String(), game) } /* generated by python code: for i in range(0,10): print((i+1)**(3.694692926)+49, ',') rationale: give brackets in an exponential range between 50 and 5000, dividing it into 10 steps. "magic constant" obtained solving for in c in the equation: 5000=(x+1)^c+49 (x = steps, 10 in our case) which comes out to be ln(delta)/ln(steps), delta = 5000-49, steps = 10. */ var bracketSize = [...]float64{ 50.0, 61.9483191543645, 106.91839582826664, 216.65896892328266, 431.3662312611604, 798.94587409321, 1374.4939512498888, 2219.9018387103433, 3403.5405753197747, 5000, } func LobbyGameFound(cur realm) string { refreshLobby(tcLobby5p0) refreshLobby(tcLobby10p5) val, ok := lobbyPlayer2Game.Get(std.PreviousRealm().Address().String()) if !ok { return "null" } return val.(*Game).json() } func LobbyQuit(cur realm) { caller := std.PreviousRealm().Address() for tc, sublob := range lobby { for i, pl := range sublob { if pl.player.Address == caller { newLobby := append([]lobbyPlayer{}, sublob[:i]...) newLobby = append(newLobby, sublob[i+1:]...) lobby[tc] = newLobby lobbyPlayer2Game.Remove(caller.String()) return } } } panic("you are not in the lobby") }