예제 #1
0
class HACModel(object):
    """

    Parameters
    ----------
    is_symmetric : bool, optional
        Defaults to False


    Attributes
    ----------

    _similarity : ValueSortedDict
    _models : dict

    """

    def __init__(self, is_symmetric=False):
        super(HACModel, self).__init__()
        self.is_symmetric = is_symmetric

    def __getitem__(self, cluster):
        return self._models[cluster]

    # models

    def compute_model(self, cluster, parent=None):
        """Compute model of cluster given current parent state

        Parameters
        ----------
        cluster : hashable
            Cluster identifier
        parent : HierarchicalAgglomerativeClustering, optional

        Returns
        -------
        model : anything
            Cluster model
        """
        raise NotImplementedError('Missing method compute_model')

    def compute_merged_model(self, clusters, parent=None):
        raise NotImplementedError('Missing method compute_merged_model')

    # 1 vs. 1 similarity

    def compute_similarity(self, cluster1, cluster2, parent=None):
        raise NotImplementedError('Missing method compute_similarity')

    # 1 vs. N similarity

    def compute_similarities(self, cluster, clusters, parent=None):
        raise NotImplementedError('')

    # N vs. N similarity

    def compute_similarity_matrix(self, parent=None):
        raise NotImplementedError('')

    def initialize(self, parent=None):

        # one model per cluster in current_state
        self._models = {}
        for cluster in parent.current_state.labels():
            self._models[cluster] = self.compute_model(cluster, parent=parent)

        # list of clusters
        clusters = list(self._models)

        try:
            self._similarity = self.compute_similarity_matrix(parent=parent)

        except NotImplementedError as e:

            n_clusters = len(clusters)

            self._similarity = ValueSortedDict()

            for i, j in combinations(clusters, 2):

                # compute similarity if (and only if) clusters are mergeable
                if not parent.constraint.mergeable([i, j], parent=parent):
                    self._similarity[i, j] = -np.inf
                    self._similarity[j, i] = -np.inf
                    continue

                similarity = self.compute_similarity(i, j, parent=parent)
                self._similarity[i, j] = similarity

                if not self.is_symmetric:
                    similarity = self.compute_similarity(j, i, parent=parent)

                self._similarity[j, i] = similarity

    # NOTE - for now this (get_candidates / block) combination assumes
    # that we merge clusters two-by-two...

    def get_candidates(self, parent=None):
        """
        Returns
        -------
        clusters : tuple
        similarity : float

        """
        return self._similarity.peekitem(index=-1)

    def block(self, clusters, parent=None):
        if len(clusters) > 2:
            raise NotImplementedError(
                'Constrained clustering merging 3+ clusters is not supported.'
            )
        i, j = clusters
        self._similarity[i, j] = -np.inf
        self._similarity[j, i] = -np.inf

    def update(self, merged_clusters, into, parent=None):

        # compute merged model
        self._models[into] = self.compute_merged_model(merged_clusters,
                                                       parent=parent)

        # remove old models and corresponding similarity
        removed_clusters = list(set(merged_clusters) - set([into]))
        for cluster in removed_clusters:
            del self._models[cluster]

        for i, j in product(removed_clusters, self._models):
            self._similarity.pop((i, j), default=None)
            self._similarity.pop((j, i), default=None)

        # compute new similarities
        # * all at once if model implements compute_similarities
        # * one by one otherwise

        remaining_clusters = list(set(self._models) - set([into]))

        if not remaining_clusters:
            return

        try:

            # all at once (when available)
            similarities = self.compute_similarities(
                into, remaining_clusters, parent=parent)

        except NotImplementedError as e:

            similarities = dict()

            for cluster in remaining_clusters:

                # compute similarity if (and only if) clusters are mergeable
                if not parent.constraint.mergeable([into, cluster], parent=parent):
                    continue

                similarity = self.compute_similarity(into, cluster, parent=parent)
                similarities[into, cluster] = similarity

                if not self.is_symmetric:
                    similarity = self.compute_similarity(cluster, into, parent=parent)
                similarities[cluster, into] = similarity

        self._similarity.update(similarities)
예제 #2
0
class Tournament(ABC):
    """Abstract base class for Arena/Swisss/RR Tournament classes
    They have to implement create_pairing() for waiting_players"""

    system: ClassVar[int] = ARENA

    def __init__(
        self,
        app,
        tournamentId,
        variant="chess",
        chess960=False,
        rated=True,
        before_start=5,
        minutes=45,
        name="",
        description="",
        fen="",
        base=1,
        inc=0,
        byoyomi_period=0,
        rounds=0,
        created_by="",
        created_at=None,
        starts_at=None,
        status=None,
        with_clock=True,
        frequency="",
    ):
        self.app = app
        self.id = tournamentId
        self.name = name
        self.description = description
        self.variant = variant
        self.rated = rated
        self.before_start = before_start  # in minutes
        self.minutes = minutes  # in minutes
        self.fen = fen
        self.base = base
        self.inc = inc
        self.byoyomi_period = byoyomi_period
        self.chess960 = chess960
        self.rounds = rounds
        self.frequency = frequency

        self.created_by = created_by
        self.created_at = datetime.now(timezone.utc) if created_at is None else created_at
        if starts_at == "" or starts_at is None:
            self.starts_at = self.created_at + timedelta(seconds=int(before_start * 60))
        else:
            self.starts_at = starts_at

        # TODO: calculate wave from TC, variant, number of players
        self.wave = timedelta(seconds=3)
        self.wave_delta = timedelta(seconds=1)
        self.current_round = 0
        self.prev_pairing = None

        self.messages = collections.deque([], MAX_CHAT_LINES)
        self.spectators = set()
        self.players: dict[User, PlayerData] = {}
        self.leaderboard = ValueSortedDict(neg)
        self.leaderboard_keys_view = SortedKeysView(self.leaderboard)
        self.status = T_CREATED if status is None else status
        self.ongoing_games = 0
        self.nb_players = 0

        self.nb_games_finished = 0
        self.w_win = 0
        self.b_win = 0
        self.draw = 0
        self.nb_berserk = 0

        self.nb_games_cached = -1
        self.leaderboard_cache = {}

        self.first_pairing = False
        self.top_player = None
        self.top_game = None

        self.notify1 = False
        self.notify2 = False

        if minutes is None:
            self.ends_at = self.starts_at + timedelta(days=1)
        else:
            self.ends_at = self.starts_at + timedelta(minutes=minutes)

        if with_clock:
            self.clock_task = asyncio.create_task(self.clock())

    def __repr__(self):
        return " ".join((self.id, self.name, self.created_at.isoformat()))

    @abstractmethod
    def create_pairing(self, waiting_players):
        pass

    def user_status(self, user):
        if user in self.players:
            return (
                "paused"
                if self.players[user].paused
                else "withdrawn"
                if self.players[user].withdrawn
                else "joined"
            )
        else:
            return "spectator"

    def user_rating(self, user):
        if user in self.players:
            return self.players[user].rating
        else:
            return "%s%s" % user.get_rating(self.variant, self.chess960).rating_prov

    def players_json(self, page=None, user=None):
        if (page is None) and (user is not None) and (user in self.players):
            if self.players[user].page > 0:
                page = self.players[user].page
            else:
                div, mod = divmod(self.leaderboard.index(user) + 1, 10)
                page = div + (1 if mod > 0 else 0)
                if self.status == T_CREATED:
                    self.players[user].page = page
        if page is None:
            page = 1

        if self.nb_games_cached != self.nb_games_finished:
            # number of games changed (game ended)
            self.leaderboard_cache = {}
            self.nb_games_cached = self.nb_games_finished
        elif user is not None:
            if self.status == T_STARTED:
                # player status changed (JOIN/PAUSE)
                if page in self.leaderboard_cache:
                    del self.leaderboard_cache[page]
            elif self.status == T_CREATED:
                # number of players changed (JOIN/WITHDRAW)
                self.leaderboard_cache = {}

        if page in self.leaderboard_cache:
            return self.leaderboard_cache[page]

        def player_json(player, full_score):
            return {
                "paused": self.players[player].paused if self.status == T_STARTED else False,
                "title": player.title,
                "name": player.username,
                "rating": self.players[player].rating,
                "points": self.players[player].points,
                "fire": self.players[player].win_streak,
                "score": full_score,  # SCORE_SHIFT-ed + performance rating
                "perf": self.players[player].performance,
                "nbGames": self.players[player].nb_games,
                "nbWin": self.players[player].nb_win,
                "nbBerserk": self.players[player].nb_berserk,
            }

        start = (page - 1) * 10
        end = min(start + 10, self.nb_players)

        page_json = {
            "type": "get_players",
            "requestedBy": user.username if user is not None else "",
            "nbPlayers": self.nb_players,
            "nbGames": self.nb_games_finished,
            "page": page,
            "players": [
                player_json(player, full_score)
                for player, full_score in self.leaderboard.items()[start:end]
            ],
        }

        if self.status > T_STARTED:
            page_json["podium"] = [
                player_json(player, full_score)
                for player, full_score in self.leaderboard.items()[0:3]
            ]

        self.leaderboard_cache[page] = page_json
        return page_json

    # TODO: cache this
    def games_json(self, player_name):
        player = self.app["users"].get(player_name)
        return {
            "type": "get_games",
            "rank": self.leaderboard.index(player) + 1,
            "title": player.title,
            "name": player_name,
            "perf": self.players[player].performance,
            "nbGames": self.players[player].nb_games,
            "nbWin": self.players[player].nb_win,
            "nbBerserk": self.players[player].nb_berserk,
            "games": [game.game_json(player) for game in self.players[player].games],
        }

    @property
    def spectator_list(self):
        return spectators(self)

    @property
    def top_game_json(self):
        return {
            "type": "top_game",
            "gameId": self.top_game.id,
            "variant": self.top_game.variant,
            "fen": self.top_game.board.fen,
            "w": self.top_game.wplayer.username,
            "b": self.top_game.bplayer.username,
            "wr": self.leaderboard_keys_view.index(self.top_game.wplayer) + 1,
            "br": self.leaderboard_keys_view.index(self.top_game.bplayer) + 1,
            "chess960": self.top_game.chess960,
            "base": self.top_game.base,
            "inc": self.top_game.inc,
            "byoyomi": self.top_game.byoyomi_period,
        }

    def waiting_players(self):
        return [
            p
            for p in self.leaderboard
            if self.players[p].free
            and self.id in p.tournament_sockets
            and len(p.tournament_sockets[self.id]) > 0
            and not self.players[p].paused
            and not self.players[p].withdrawn
        ]

    async def clock(self):
        try:
            while self.status not in (T_ABORTED, T_FINISHED, T_ARCHIVED):
                now = datetime.now(timezone.utc)

                if self.status == T_CREATED:
                    remaining_time = self.starts_at - now
                    remaining_mins_to_start = int(
                        ((remaining_time.days * 3600 * 24) + remaining_time.seconds) / 60
                    )
                    if now >= self.starts_at:
                        if self.system != ARENA and len(self.players) < 3:
                            # Swiss and RR Tournaments need at least 3 players to start
                            await self.abort()
                            print("T_ABORTED: less than 3 player joined")
                            break

                        await self.start(now)
                        continue

                    elif (not self.notify2) and remaining_mins_to_start <= NOTIFY2_MINUTES:
                        self.notify1 = True
                        self.notify2 = True
                        await discord_message(
                            self.app,
                            "notify_tournament",
                            self.notify_discord_msg(remaining_mins_to_start),
                        )
                        continue

                    elif (not self.notify1) and remaining_mins_to_start <= NOTIFY1_MINUTES:
                        self.notify1 = True
                        await discord_message(
                            self.app,
                            "notify_tournament",
                            self.notify_discord_msg(remaining_mins_to_start),
                        )
                        continue

                elif (self.minutes is not None) and now >= self.ends_at:
                    await self.finish()
                    print("T_FINISHED: no more time left")
                    break

                elif self.status == T_STARTED:
                    if self.system == ARENA:
                        # In case of server restart
                        if self.prev_pairing is None:
                            self.prev_pairing = now - self.wave

                        if now >= self.prev_pairing + self.wave + random.uniform(
                            -self.wave_delta, self.wave_delta
                        ):
                            waiting_players = self.waiting_players()
                            nb_waiting_players = len(waiting_players)
                            if nb_waiting_players >= 2:
                                log.debug("Enough player (%s), do pairing", nb_waiting_players)
                                await self.create_new_pairings(waiting_players)
                                self.prev_pairing = now
                            else:
                                log.debug(
                                    "Too few player (%s) to make pairing",
                                    nb_waiting_players,
                                )
                        else:
                            log.debug("Waiting for new pairing wave...")

                    elif self.ongoing_games == 0:
                        if self.current_round < self.rounds:
                            self.current_round += 1
                            log.debug("Do %s. round pairing", self.current_round)
                            waiting_players = self.waiting_players()
                            await self.create_new_pairings(waiting_players)
                        else:
                            await self.finish()
                            log.debug("T_FINISHED: no more round left")
                            break
                    else:
                        print(
                            "%s has %s ongoing game(s)..."
                            % (
                                "RR" if self.system == RR else "Swiss",
                                self.ongoing_games,
                            )
                        )

                    log.debug("%s CLOCK %s", self.id, now.strftime("%H:%M:%S"))
                await asyncio.sleep(1)
        except Exception:
            log.exception("Exception in tournament clock()")

    async def start(self, now):
        self.status = T_STARTED

        self.first_pairing = True
        self.set_top_player()

        response = {
            "type": "tstatus",
            "tstatus": self.status,
            "secondsToFinish": (self.ends_at - now).total_seconds(),
        }
        await self.broadcast(response)

        # force first pairing wave in arena
        if self.system == ARENA:
            self.prev_pairing = now - self.wave

        if self.app["db"] is not None:
            print(
                await self.app["db"].tournament.find_one_and_update(
                    {"_id": self.id},
                    {"$set": {"status": self.status}},
                    return_document=ReturnDocument.AFTER,
                )
            )

    @property
    def summary(self):
        return {
            "type": "tstatus",
            "tstatus": self.status,
            "nbPlayers": self.nb_players,
            "nbGames": self.nb_games_finished,
            "wWin": self.w_win,
            "bWin": self.b_win,
            "draw": self.draw,
            "berserk": self.nb_berserk,
            "sumRating": sum(
                self.players[player].rating
                for player in self.players
                if not self.players[player].withdrawn
            ),
        }

    async def finalize(self, status):
        self.status = status

        if len(self.players) > 0:
            self.print_leaderboard()
            print("--- TOURNAMENT RESULT ---")
            for i in range(min(3, len(self.leaderboard))):
                player = self.leaderboard.peekitem(i)[0]
                print("--- #%s ---" % (i + 1), player.username)

        # remove latest games from players tournament if it was not finished in time
        for player in self.players:
            if len(self.players[player].games) == 0:
                continue
            latest = self.players[player].games[-1]
            if latest and latest.status in (CREATED, STARTED):
                self.players[player].games.pop()
                self.players[player].points.pop()
                self.players[player].nb_games -= 1

        # force to create new players json data
        self.nb_games_cached = -1

        await self.broadcast(self.summary)
        await self.save()

        await self.broadcast_spotlight()

    async def broadcast_spotlight(self):
        spotlights = tournament_spotlights(self.app["tournaments"])
        lobby_sockets = self.app["lobbysockets"]
        response = {"type": "spotlights", "items": spotlights}
        await lobby_broadcast(lobby_sockets, response)

    async def abort(self):
        await self.finalize(T_ABORTED)

    async def finish(self):
        await self.finalize(T_FINISHED)

    async def join(self, user):
        if user.anon:
            return

        if self.system == RR and len(self.players) > self.rounds + 1:
            raise EnoughPlayer

        if user not in self.players:
            # new player joined
            rating, provisional = user.get_rating(self.variant, self.chess960).rating_prov
            self.players[user] = PlayerData(rating, provisional)
        elif self.players[user].withdrawn:
            # withdrawn player joined again
            rating, provisional = user.get_rating(self.variant, self.chess960).rating_prov

        if user not in self.leaderboard:
            # new player joined or withdrawn player joined again
            if self.status == T_CREATED:
                self.leaderboard.setdefault(user, rating)
            else:
                self.leaderboard.setdefault(user, 0)
            self.nb_players += 1

        self.players[user].paused = False
        self.players[user].withdrawn = False

        response = self.players_json(user=user)
        await self.broadcast(response)

        if self.status == T_CREATED:
            await self.broadcast_spotlight()

        await self.db_update_player(user, self.players[user])

    async def withdraw(self, user):
        self.players[user].withdrawn = True

        self.leaderboard.pop(user)
        self.nb_players -= 1

        response = self.players_json(user=user)
        await self.broadcast(response)

        await self.broadcast_spotlight()

        await self.db_update_player(user, self.players[user])

    async def pause(self, user):
        self.players[user].paused = True

        # pause is different from withdraw and join because pause can be initiated from finished games page as well
        response = self.players_json(user=user)
        await self.broadcast(response)

        if (self.top_player is not None) and self.top_player.username == user.username:
            self.set_top_player()

        await self.db_update_player(user, self.players[user])

    def spactator_join(self, spectator):
        self.spectators.add(spectator)

    def spactator_leave(self, spectator):
        self.spectators.discard(spectator)

    async def create_new_pairings(self, waiting_players):
        pairing = self.create_pairing(waiting_players)

        if self.first_pairing:
            self.first_pairing = False
            # Before tournament starts leaderboard is ordered by ratings
            # After first pairing it will be sorted by score points and performance
            # so we have to make a clear (all 0) leaderboard here
            new_leaderboard = [(user, 0) for user in self.leaderboard]
            self.leaderboard = ValueSortedDict(neg, new_leaderboard)
            self.leaderboard_keys_view = SortedKeysView(self.leaderboard)

        games = await self.create_games(pairing)
        return (pairing, games)

    def set_top_player(self):
        idx = 0
        self.top_player = None
        while idx < self.nb_players:
            top_player = self.leaderboard.peekitem(idx)[0]
            if self.players[top_player].paused:
                idx += 1
                continue
            else:
                self.top_player = top_player
                break

    async def create_games(self, pairing):
        check_top_game = self.top_player is not None
        new_top_game = False

        games = []
        game_table = None if self.app["db"] is None else self.app["db"].game
        for wp, bp in pairing:
            game_id = await new_id(game_table)
            game = Game(
                self.app,
                game_id,
                self.variant,
                self.fen,
                wp,
                bp,
                base=self.base,
                inc=self.inc,
                byoyomi_period=self.byoyomi_period,
                rated=RATED if self.rated else CASUAL,
                tournamentId=self.id,
                chess960=self.chess960,
            )

            games.append(game)
            self.app["games"][game_id] = game
            await insert_game_to_db(game, self.app)

            # TODO: save new game to db
            if 0:  # self.app["db"] is not None:
                doc = {
                    "_id": game.id,
                    "tid": self.id,
                    "u": [game.wplayer.username, game.bplayer.username],
                    "r": "*",
                    "d": game.date,
                    "wr": game.wrating,
                    "br": game.brating,
                }
                await self.app["db"].tournament_pairing.insert_one(doc)

            self.players[wp].games.append(game)
            self.players[bp].games.append(game)

            self.players[wp].points.append("*")
            self.players[bp].points.append("*")

            self.ongoing_games += 1

            self.players[wp].free = False
            self.players[bp].free = False

            self.players[wp].nb_games += 1
            self.players[bp].nb_games += 1

            self.players[wp].prev_opp = game.bplayer.username
            self.players[bp].prev_opp = game.wplayer.username

            self.players[wp].color_balance += 1
            self.players[bp].color_balance -= 1

            self.players[wp].nb_not_paired = 0
            self.players[bp].nb_not_paired = 0

            response = {
                "type": "new_game",
                "gameId": game_id,
                "wplayer": wp.username,
                "bplayer": bp.username,
            }

            try:
                ws = next(iter(wp.tournament_sockets[self.id]))
                if ws is not None:
                    await ws.send_json(response)
            except Exception:
                self.pause(wp)
                log.debug("White player %s left the tournament", wp.username)

            try:
                ws = next(iter(bp.tournament_sockets[self.id]))
                if ws is not None:
                    await ws.send_json(response)
            except Exception:
                self.pause(bp)
                log.debug("Black player %s left the tournament", bp.username)

            if (
                check_top_game
                and (self.top_player is not None)
                and self.top_player.username in (game.wplayer.username, game.bplayer.username)
                and game.status != BYEGAME
            ):  # Bye game
                self.top_game = game
                check_top_game = False
                new_top_game = True

        if new_top_game:
            tgj = self.top_game_json
            await self.broadcast(tgj)

        return games

    def points_perfs(self, game: Game) -> Tuple[Point, Point, int, int]:
        wplayer = self.players[game.wplayer]
        bplayer = self.players[game.bplayer]

        wpoint = (0, SCORE)
        bpoint = (0, SCORE)
        wperf = game.black_rating.rating_prov[0]
        bperf = game.white_rating.rating_prov[0]

        if game.result == "1/2-1/2":
            if self.system == ARENA:
                if game.board.ply > 10:
                    wpoint = (2, SCORE) if wplayer.win_streak == 2 else (1, SCORE)
                    bpoint = (2, SCORE) if bplayer.win_streak == 2 else (1, SCORE)

                wplayer.win_streak = 0
                bplayer.win_streak = 0
            else:
                wpoint, bpoint = (1, SCORE), (1, SCORE)

        elif game.result == "1-0":
            wplayer.nb_win += 1
            if self.system == ARENA:
                if wplayer.win_streak == 2:
                    wpoint = (4, DOUBLE)
                else:
                    wplayer.win_streak += 1
                    wpoint = (2, STREAK if wplayer.win_streak == 2 else SCORE)

                bplayer.win_streak = 0
            else:
                wpoint = (2, SCORE)

            if game.wberserk and game.board.ply >= 13:
                wpoint = (wpoint[0] + 1, wpoint[1])

            wperf += 500
            bperf -= 500

        elif game.result == "0-1":
            bplayer.nb_win += 1
            if self.system == ARENA:
                if bplayer.win_streak == 2:
                    bpoint = (4, DOUBLE)
                else:
                    bplayer.win_streak += 1
                    bpoint = (2, STREAK if bplayer.win_streak == 2 else SCORE)

                wplayer.win_streak = 0
            else:
                bpoint = (2, SCORE)

            if game.bberserk and game.board.ply >= 14:
                bpoint = (bpoint[0] + 1, bpoint[1])

            wperf -= 500
            bperf += 500

        return (wpoint, bpoint, wperf, bperf)

    def points_perfs_janggi(self, game):
        wplayer = self.players[game.wplayer]
        bplayer = self.players[game.bplayer]

        wpoint = (0, SCORE)
        bpoint = (0, SCORE)
        wperf = game.black_rating.rating_prov[0]
        bperf = game.white_rating.rating_prov[0]

        if game.status == VARIANTEND:
            wplayer.win_streak = 0
            bplayer.win_streak = 0

            if game.result == "1-0":
                if self.system == ARENA:
                    wpoint = (4 * 2 if wplayer.win_streak == 2 else 4, SCORE)
                    bpoint = (2 * 2 if bplayer.win_streak == 2 else 2, SCORE)
                else:
                    wpoint = (4, SCORE)
                    bpoint = (2, SCORE)

            elif game.result == "0-1":
                if self.system == ARENA:
                    bpoint = (4 * 2 if bplayer.win_streak == 2 else 4, SCORE)
                    wpoint = (2 * 2 if wplayer.win_streak == 2 else 2, SCORE)
                else:
                    bpoint = (4, SCORE)
                    wpoint = (2, SCORE)

        elif game.result == "1-0":
            wplayer.nb_win += 1
            if self.system == ARENA:
                if wplayer.win_streak == 2:
                    wpoint = (7 * 2, DOUBLE)
                else:
                    wplayer.win_streak += 1
                    wpoint = (7, STREAK if wplayer.win_streak == 2 else SCORE)

                bplayer.win_streak = 0

                if game.wberserk and game.board.ply >= 13:
                    wpoint = (wpoint[0] + 3, wpoint[1])
            else:
                wpoint = (7, SCORE)
                bpoint = (0, SCORE)

            wperf += 500
            bperf -= 500

        elif game.result == "0-1":
            bplayer.nb_win += 1
            if self.system == ARENA:
                if bplayer.win_streak == 2:
                    bpoint = (7 * 2, DOUBLE)
                else:
                    bplayer.win_streak += 1
                    bpoint = (7, STREAK if bplayer.win_streak == 2 else SCORE)

                wplayer.win_streak = 0

                if game.bberserk and game.board.ply >= 14:
                    bpoint = (bpoint[0] + 3, bpoint[1])
            else:
                wpoint = (0, SCORE)
                bpoint = (7, SCORE)

            wperf -= 500
            bperf += 500

        return (wpoint, bpoint, wperf, bperf)

    async def game_update(self, game):
        """Called from Game.update_status()"""
        if self.status == T_FINISHED and self.status != T_ARCHIVED:
            return

        wplayer = self.players[game.wplayer]
        bplayer = self.players[game.bplayer]

        if game.wberserk:
            wplayer.nb_berserk += 1
            self.nb_berserk += 1

        if game.bberserk:
            bplayer.nb_berserk += 1
            self.nb_berserk += 1

        if game.variant == "janggi":
            wpoint, bpoint, wperf, bperf = self.points_perfs_janggi(game)
        else:
            wpoint, bpoint, wperf, bperf = self.points_perfs(game)

        wplayer.points[-1] = wpoint
        bplayer.points[-1] = bpoint
        if wpoint[1] == STREAK and len(wplayer.points) >= 2:
            wplayer.points[-2] = (wplayer.points[-2][0], STREAK)
        if bpoint[1] == STREAK and len(bplayer.points) >= 2:
            bplayer.points[-2] = (bplayer.points[-2][0], STREAK)

        wplayer.rating = game.white_rating.rating_prov[0] + (int(game.wrdiff) if game.wrdiff else 0)
        bplayer.rating = game.black_rating.rating_prov[0] + (int(game.brdiff) if game.brdiff else 0)

        # TODO: in Swiss we will need Berger instead of performance to calculate tie breaks
        nb = wplayer.nb_games
        wplayer.performance = int(round((wplayer.performance * (nb - 1) + wperf) / nb, 0))

        nb = bplayer.nb_games
        bplayer.performance = int(round((bplayer.performance * (nb - 1) + bperf) / nb, 0))

        wpscore = self.leaderboard.get(game.wplayer) // SCORE_SHIFT
        self.leaderboard.update(
            {game.wplayer: SCORE_SHIFT * (wpscore + wpoint[0]) + wplayer.performance}
        )

        bpscore = self.leaderboard.get(game.bplayer) // SCORE_SHIFT
        self.leaderboard.update(
            {game.bplayer: SCORE_SHIFT * (bpscore + bpoint[0]) + bplayer.performance}
        )

        self.nb_games_finished += 1

        if game.result == "1-0":
            self.w_win += 1
        elif game.result == "0-1":
            self.b_win += 1
        elif game.result == "1/2-1/2":
            self.draw += 1

        asyncio.create_task(self.delayed_free(game, wplayer, bplayer))

        # TODO: save player points to db
        # await self.db_update_player(wplayer, self.players[wplayer])
        # await self.db_update_player(bplayer, self.players[bplayer])

        self.set_top_player()

        await self.broadcast(
            {
                "type": "game_update",
                "wname": game.wplayer.username,
                "bname": game.bplayer.username,
            }
        )

        if self.top_game is not None and self.top_game.id == game.id:
            response = {
                "type": "gameEnd",
                "status": game.status,
                "result": game.result,
                "gameId": game.id,
            }
            await self.broadcast(response)

            if (self.top_player is not None) and self.top_player.username not in (
                game.wplayer.username,
                game.bplayer.username,
            ):
                top_game_candidate = self.players[self.top_player].games[-1]
                if top_game_candidate.status != BYEGAME:
                    self.top_game = top_game_candidate
                    if (self.top_game is not None) and (self.top_game.status <= STARTED):
                        tgj = self.top_game_json
                        await self.broadcast(tgj)

    async def delayed_free(self, game, wplayer, bplayer):
        if self.system == ARENA:
            await asyncio.sleep(3)

        wplayer.free = True
        bplayer.free = True

        if game.status == FLAG:
            # pause players when they don't start their game
            if game.board.ply == 0:
                wplayer.paused = True
            elif game.board.ply == 1:
                bplayer.paused = True

        self.ongoing_games -= 1

    async def broadcast(self, response):
        for spectator in self.spectators:
            try:
                for ws in spectator.tournament_sockets[self.id]:
                    try:
                        await ws.send_json(response)
                    except ConnectionResetError:
                        pass
            except KeyError:
                # spectator was removed
                pass
            except Exception:
                log.exception("Exception in tournament broadcast()")

    async def db_update_player(self, user, player_data):
        if self.app["db"] is None:
            return

        player_id = player_data.id
        player_table = self.app["db"].tournament_player

        if player_data.id is None:  # new player join
            player_id = await new_id(player_table)
            player_data.id = player_id

        if player_data.withdrawn:
            new_data = {
                "wd": True,
            }
        else:
            full_score = self.leaderboard[user]
            new_data = {
                "_id": player_id,
                "tid": self.id,
                "uid": user.username,
                "r": player_data.rating,
                "pr": player_data.provisional,
                "a": player_data.paused,
                "f": player_data.win_streak == 2,
                "s": int(full_score / SCORE_SHIFT),
                "g": player_data.nb_games,
                "w": player_data.nb_win,
                "b": player_data.nb_berserk,
                "e": player_data.performance,
                "p": player_data.points,
                "wd": False,
            }

        try:
            print(
                await player_table.find_one_and_update(
                    {"_id": player_id},
                    {"$set": new_data},
                    upsert=True,
                    return_document=ReturnDocument.AFTER,
                )
            )
        except Exception:
            if self.app["db"] is not None:
                log.error(
                    "db find_one_and_update tournament_player %s into %s failed !!!",
                    player_id,
                    self.id,
                )

        new_data = {"nbPlayers": self.nb_players, "nbBerserk": self.nb_berserk}
        print(
            await self.app["db"].tournament.find_one_and_update(
                {"_id": self.id},
                {"$set": new_data},
                return_document=ReturnDocument.AFTER,
            )
        )

    async def save(self):
        if self.app["db"] is None:
            return

        if self.nb_games_finished == 0:
            print(await self.app["db"].tournament.delete_many({"_id": self.id}))
            print("--- Deleted empty tournament %s" % self.id)
            return

        winner = self.leaderboard.peekitem(0)[0].username
        new_data = {
            "status": self.status,
            "nbPlayers": self.nb_players,
            "nbGames": self.nb_games_finished,
            "winner": winner,
        }

        print(
            await self.app["db"].tournament.find_one_and_update(
                {"_id": self.id},
                {"$set": new_data},
                return_document=ReturnDocument.AFTER,
            )
        )

        pairing_documents = []
        pairing_table = self.app["db"].tournament_pairing

        processed_games = set()

        for user, user_data in self.players.items():
            for game in user_data.games:
                if game.status == BYEGAME:  # ByeGame
                    continue
                if game.id not in processed_games:
                    pairing_documents.append(
                        {
                            "_id": game.id,
                            "tid": self.id,
                            "u": (game.wplayer.username, game.bplayer.username),
                            "r": R2C[game.result],
                            "d": game.date,
                            "wr": game.wrating,
                            "br": game.brating,
                            "wb": game.wberserk,
                            "bb": game.bberserk,
                        }
                    )
                processed_games.add(game.id)

        await pairing_table.insert_many(pairing_documents)

        for user in self.leaderboard:
            await self.db_update_player(user, self.players[user])

        if self.frequency == SHIELD:
            variant_name = self.variant + ("960" if self.chess960 else "")
            self.app["shield"][variant_name].append((winner, self.starts_at, self.id))
            self.app["shield_owners"][variant_name] = winner

    def print_leaderboard(self):
        print("--- LEADERBOARD ---", self.id)
        for player, full_score in self.leaderboard.items()[:10]:
            print(
                "%20s %4s %30s %2s %s"
                % (
                    player.username,
                    self.players[player].rating,
                    self.players[player].points,
                    full_score,
                    self.players[player].performance,
                )
            )

    @property
    def create_discord_msg(self):
        tc = time_control_str(self.base, self.inc, self.byoyomi_period)
        tail960 = "960" if self.chess960 else ""
        return "%s: **%s%s** %s tournament starts at UTC %s, duration will be **%s** minutes" % (
            self.created_by,
            self.variant,
            tail960,
            tc,
            self.starts_at.strftime("%Y.%m.%d %H:%M"),
            self.minutes,
        )

    def notify_discord_msg(self, minutes):
        tc = time_control_str(self.base, self.inc, self.byoyomi_period)
        tail960 = "960" if self.chess960 else ""
        url = "https://www.pychess.org/tournament/%s" % self.id
        if minutes >= 60:
            time = int(minutes / 60)
            time_text = "hours"
        else:
            time = minutes
            time_text = "minutes"
        return "**%s%s** %s tournament starts in **%s** %s! %s" % (
            self.variant,
            tail960,
            tc,
            time,
            time_text,
            url,
        )
예제 #3
0
    def cluster_(self, fX):
        """Compute complete dendrogram

        Parameters
        ----------
        fX : (n_items, dimension) np.array
            Embeddings.

        Returns
        -------
        dendrogram : list of (i, j, distance) tuples
            Dendrogram.
        """

        N = len(fX)

        # clusters contain the identifier of each cluster
        clusters = SortedSet(np.arange(N))

        # labels[i] = c means ith item belongs to cluster c
        labels = np.array(np.arange(N))

        squared = squareform(pdist(fX, metric=self.metric))
        distances = ValueSortedDict()
        for i, j in itertools.combinations(range(N), 2):
            distances[i, j] = squared[i, j]

        dendrogram = []

        for _ in range(N-1):

            # find most similar clusters
            (c_i, c_j), d = distances.peekitem(index=0)

            # keep track of this iteration
            dendrogram.append((c_i, c_j, d))

            # index of clusters in 'clusters' and 'fX'
            i = clusters.index(c_i)
            j = clusters.index(c_j)

            # merge items of cluster c_j into cluster c_i
            labels[labels == c_j] = c_i

            # update c_i representative
            fX[i] += fX[j]

            # remove c_j cluster
            fX[j:-1, :] = fX[j+1:, :]
            fX = fX[:-1]

            # remove distances to c_j cluster
            for c in clusters[:j]:
                distances.pop((c, c_j))
            for c in clusters[j+1:]:
                distances.pop((c_j, c))

            clusters.remove(c_j)

            if len(clusters) < 2:
                continue

            # compute distance to new c_i cluster
            new_d = cdist(fX[i, :].reshape((1, -1)), fX, metric=self.metric).squeeze()
            for c_k, d in zip(clusters, new_d):

                if c_k < c_i:
                    distances[c_k, c_i] = d
                elif c_k > c_i:
                    distances[c_i, c_k] = d

        return dendrogram