Пример #1
0
class DotaBot(Greenlet):
    """A worker thread, connecting to steam to process a unique job.

    Attributes:
        worker_manager: `DazzarWorkerManager` this bot is linked to.
        credential: `Credential` used to connect to steam.
    """
    def __init__(self, worker_manager, credential, job):
        """Initialize the Dota bot thread for a unique job process.

        Args:
            worker_manager: `DazzarWorkerManager` this bot is linked to.
            credential: `Credential` used to connect to steam.
            job: `Job` to process by the bot.
        """
        Greenlet.__init__(self)

        self.credential = credential
        self.worker_manager = worker_manager
        self.job = job

        self.client = SteamClient()
        self.dota = dota2.Dota2Client(self.client)
        self.app = self.worker_manager.app

        self.job_started = False
        self.game_creation_call = False
        self.job_finished = False

        self.match = None
        self.players = None

        self.game_status = None
        self.lobby_channel_id = None
        self.invite_timer = None
        self.missing_players = None
        self.missing_players_count = None
        self.wrong_team_players = None
        self.wrong_team_players_count = None

        # Prepare all event handlers
        # - Steam client events
        # - Dota client events
        # - Dazzar bot events
        self.client.on('connected', self.steam_connected)
        self.client.on('logged_on', self.steam_logged)

        self.dota.on('ready', self.dota_ready)
        self.dota.on('notready', self.closed_dota)

        self.dota.on('profile_card', self.scan_profile_result)
        self.dota.on('player_info', self.scan_player_info)
        self.dota.on(dota2.features.Lobby.EVENT_LOBBY_NEW,
                     self.vip_game_created)
        self.dota.on(dota2.features.Lobby.EVENT_LOBBY_CHANGED,
                     self.game_update)
        self.dota.on(dota2.features.Chat.EVENT_CHANNEL_JOIN, self.channel_join)
        self.dota.on(dota2.features.Chat.EVENT_CHANNEL_MESSAGE,
                     self.channel_message)

    def _run(self):
        """Start the main loop of the thread, connecting to Steam, waiting for the job to finish to close the bot."""
        self.print_info('Connecting to Steam...')
        self.client.connect(retry=None)  # Try connecting with infinite retries

        while not self.job_finished:
            sleep(10)

        self.client.disconnect()
        self.worker_manager.bot_end(self.credential)

    # Helpers

    def print_info(self, trace):
        """Wrapper of `logging.info` with bot name prefix.

        Args:
            trace: String to output as INFO.
        """
        logging.info('%s: %s', self.credential.login, trace)

    def print_error(self, trace):
        """Wrapper of `logging.error` with bot name prefix.

        Args:
            trace: String to output as ERROR.
        """
        logging.error("%s: %s", self.credential.login, trace)

    # Callback of Steam and Dota clients

    def steam_connected(self):
        """Callback fired when the bot is connected to Steam, login user."""
        self.print_info('Connected to Steam.')
        self.client.login(self.credential.login, self.credential.password)

    def steam_logged(self):
        """Callback fired when the bot is logged into Steam, starting Dota."""
        self.print_info('Logged to Steam.')
        self.dota.launch()

    def dota_ready(self):
        """Callback fired when the Dota application is ready, resume the job processing."""
        self.print_info('Dota application is ready.')
        self.compute_job()

    def closed_dota(self):
        """Callback fired when the Dota application is closed."""
        self.print_info('Dota application is closed.')

    def compute_job(self):
        """Start the processing of the job with the appropriate handler."""
        self.print_info('Processing new job of type %s' % type(self.job))
        if not self.job_started:
            self.job_started = True
        else:
            return

        if type(self.job) is JobScan:
            self.scan_profile()
        elif type(self.job) is JobCreateGame:
            self.vip_game()
        else:
            self.end_job_processing()

    def end_job_processing(self):
        """Mark the job as finished, preparing the bot to close."""
        self.print_info('Job ended.')
        self.job = None
        self.job_finished = True

    def channel_join(self, channel_info):
        if channel_info.channel_type != dota2.enums.DOTAChatChannelType_t.DOTAChannelType_Lobby:
            self.dota.leave_channel(channel_info.channel_id)
        else:
            if self.game_status is not None:
                if channel_info.channel_name == 'Lobby_{0}'.format(
                        self.game_status.lobby_id):
                    self.lobby_channel_id = channel_info.channel_id

    def channel_message(self, message):
        pass

    ############################
    # Scan profile job section #
    ############################

    def scan_profile(self):
        """Start the process of the job as a profile scan, request the information from Steam."""
        while not self.job.scan_finish:
            self.print_info('Requesting profile for user %s' %
                            self.job.steam_id)
            user = SteamID(self.job.steam_id)
            self.dota.request_profile_card(user.as_32)

            # We give the task 30 sec to finish or retry
            sleep(30)

        if self.job.scan_finish:
            self.end_job_processing()

    def scan_player_info(self, message):
        self.print_info(message)
        self.job.scan_finish = True

    def scan_profile_result(self, account_id, profile_card):
        """Process the profile information returned by Steam.

        Extract the SoloMMR from the profile and insert the user in the good ladder.

        Args:
            account_id: steam_id (as 32bits) of the profile result
            profile_card: profile information as a protobuff message
        """
        self.print_info('Processing profile of user %s' %
                        SteamID(account_id).as_64)
        solo_mmr = None
        for slot in profile_card.slots:
            if not slot.HasField('stat'):
                continue
            if slot.stat.stat_id != 1:
                continue
            solo_mmr = int(slot.stat.stat_score)

        with self.app.app_context():
            user = User.query.filter_by(id=self.job.steam_id).first()
            user.profile_scan_info.last_scan = datetime.utcnow()
            if solo_mmr is not None:
                user.solo_mmr = solo_mmr

            if user.solo_mmr is None:
                user.section = None
            else:
                if user.solo_mmr > 4500:
                    user.section = constants.LADDER_HIGH
                else:
                    user.section = constants.LADDER_LOW

            db.session.commit()
        self.job.scan_finish = True

    ########################
    # VIP game job section #
    ########################

    def vip_game(self):
        """Start the process of the job as a game creation."""
        self.print_info('Hosting game %s' % self.job.match_id)

        # Copy the match data from the database
        with self.app.app_context():
            self.match = Match.query.filter_by(id=self.job.match_id).first()
            if self.match is None or self.match.status != constants.MATCH_STATUS_CREATION:
                self.dota.leave_practice_lobby()
                self.end_job_processing()
            else:
                self.players = {}
                for player in PlayerInMatch.query.\
                    options(joinedload_all('player')).\
                    filter(PlayerInMatch.match_id == self.job.match_id).\
                    all():
                    self.players[player.player_id] = player

                db.session.expunge(self.match)
                for player_id, player in self.players.items():
                    db.session.expunge(player)

                # Start the Dota lobby
                self.dota.create_practice_lobby(password=self.match.password)
                self.game_creation_call = True

    def vip_game_created(self, message):
        """Callback fired when the Dota bot enters a lobby.

        Args:
            message: first lobby information
        """
        self.game_status = message

        if self.job is None or not self.game_creation_call:
            self.dota.leave_practice_lobby()
        else:
            self.initialize_lobby()
            start = self.manage_player_waiting()

            if not start:
                self.dota.send_message(self.lobby_channel_id,
                                       'Annulation de la partie.')
                self.process_game_dodge()
            else:
                self.dota.send_message(self.lobby_channel_id,
                                       'Tous les joueurs sont présents.')
                self.start_game()

                # Waiting PostGame = 3 or UI = 0 (means no loading)
                while self.game_status.state != 0 and self.game_status.state != 3:
                    sleep(5)

                if self.game_status.state == 0:
                    self.process_game_dodge()
                elif self.game_status.state == 3:
                    self.process_endgame_results()

            if self.lobby_channel_id is not None:
                self.dota.leave_channel(self.lobby_channel_id)
                self.lobby_channel_id = None

            self.dota.leave_practice_lobby()
            self.end_job_processing()

    def game_update(self, message):
        """Callback fired when the game lobby change, update local information."""
        self.game_status = message

    def initialize_lobby(self):
        """Setup the game lobby with the good options, and change status in database."""
        self.print_info('Game %s created, setup.' % self.job.match_id)
        self.dota.join_lobby_channel()
        self.dota.join_practice_lobby_team()
        game_mode_encoding = {
            'ap': dota2.enums.DOTA_GameMode.DOTA_GAMEMODE_ALL_DRAFT,
            'rd': dota2.enums.DOTA_GameMode.DOTA_GAMEMODE_RD,
            'cd': dota2.enums.DOTA_GameMode.DOTA_GAMEMODE_CD
        }

        options = {
            'game_name': 'Dazzar Game {0}'.format(str(self.match.id)),
            'pass_key': self.match.password,
            'game_mode': game_mode_encoding[self.match.mode],
            'server_region': int(dota2.enums.EServerRegion.Europe),
            'fill_with_bots': False,
            'allow_spectating': True,
            'allow_cheats': False,
            'allchat': False,
            'dota_tv_delay': 2,
            'pause_setting': 1
        }
        self.dota.config_practice_lobby(options=options)
        with self.app.app_context():
            match = Match.query.filter_by(id=self.job.match_id).first()
            match.status = constants.MATCH_STATUS_WAITING_FOR_PLAYERS
            db.session.commit()

    def manage_player_waiting(self):
        """Wait for players to join the lobby with actions depending on player actions.

        Returns:
            A boolean that indicates if the game should be started after the player waiting process.
        """
        self.invite_timer = timedelta(minutes=5)
        self.compute_player_status()
        refresh_rate = 10

        while self.invite_timer != timedelta(0):
            for player in self.missing_players:
                self.dota.invite_to_lobby(player)
            sleep(refresh_rate)
            self.compute_player_status()

            if len(self.missing_players) == 0 and len(
                    self.wrong_team_players) == 0:
                return True
            else:
                if self.invite_timer.seconds != 0 and self.invite_timer.seconds % 60 in [
                        0, 30
                ]:
                    minutes = self.invite_timer.seconds // 60
                    seconds = self.invite_timer.seconds - 60 * minutes
                    self.dota.send_message(
                        self.lobby_channel_id,
                        '{:01d}:{:02d} avant annulation, {} absent(s), {} mal placé(s).'
                        .format(minutes, seconds, self.missing_players_count,
                                self.wrong_team_players_count))
                self.invite_timer = self.invite_timer - timedelta(
                    seconds=refresh_rate)
        return False

    def compute_player_status(self):
        """Helpers to manage player status from protobuff message.

        Invite all missing players to come to the lobby.
        Kick all players not supposed to be inside a lobby.
        Kick from slots all players not in the good slot.
        """
        self.missing_players = []
        self.missing_players_count = 0
        self.wrong_team_players = []
        self.wrong_team_players_count = 0

        for player_id, player in self.players.items():
            self.missing_players_count += 1
            self.missing_players.append(player_id)

        for message_player in self.game_status.members:
            if message_player.id == self.dota.steam_id:
                continue
            if message_player.id in self.missing_players:
                self.missing_players_count -= 1
                self.missing_players.remove(message_player.id)
                good_slot = message_player.slot == self.players[
                    message_player.id].team_slot
                good_team = (message_player.team == DOTA_GC_TEAM.GOOD_GUYS and
                             self.players[message_player.id].is_radiant) or \
                            (message_player.team == DOTA_GC_TEAM.BAD_GUYS and
                             not self.players[message_player.id].is_radiant)
                if not (good_team and good_slot):
                    self.wrong_team_players.append(message_player.id)
                    self.wrong_team_players_count += 1
                    if message_player.team != DOTA_GC_TEAM.PLAYER_POOL:
                        self.dota.practice_lobby_kick_from_team(
                            SteamID(message_player.id).as_32)
            else:
                # Say: Kick joueur non authorisé message_player.name
                self.dota.practice_lobby_kick(SteamID(message_player.id).as_32)

    def process_game_dodge(self):
        """Punish players stopping game start."""
        self.print_info('Game %s cancelled because of dodge.' %
                        self.job.match_id)

        # Say: Partie annulée - punish
        with self.app.app_context():
            match = Match.query.filter_by(id=self.job.match_id).first()
            match.status = constants.MATCH_STATUS_CANCELLED
            self.compute_player_status()
            for player in PlayerInMatch.query. \
                    options(joinedload_all('player')). \
                    filter(PlayerInMatch.match_id == self.job.match_id). \
                    all():
                if player.player.current_match == self.job.match_id:
                    player.player.current_match = None

                # Update Scoreboard
                if player.player_id in self.missing_players or player.player_id in self.wrong_team_players:
                    score = Scoreboard.query.filter_by(
                        ladder_name=match.section,
                        user_id=player.player_id).first()
                    if score is None:
                        score = Scoreboard(user=player.player,
                                           ladder_name=match.section)
                        db.session.add(score)
                    player.is_dodge = True
                    score.points -= 2
                    score.dodge += 1
            db.session.commit()

    def start_game(self):
        """Start the Dota game and update status in database."""
        self.print_info('Launching game %s' % self.job.match_id)

        self.dota.launch_practice_lobby()
        sleep(10)
        with self.app.app_context():
            match = Match.query.filter_by(id=self.job.match_id).first()
            match.status = constants.MATCH_STATUS_IN_PROGRESS
            if self.game_status.connect is not None and self.game_status.connect[
                    0:1] == '=[':
                match.server = self.game_status.connect[2:-1]
            elif self.game_status.server_id is not None:
                match.server = self.game_status.server_id
            db.session.commit()
        sleep(10)

    def process_endgame_results(self):
        """After a game, process lobby results into database."""
        self.print_info('Game %s over.' % self.job.match_id)

        with self.app.app_context():
            match = Match.query.filter_by(id=self.job.match_id).first()
            match.status = constants.MATCH_STATUS_ENDED
            match.server = None
            if self.game_status.match_outcome == 2:
                match.radiant_win = True
            elif self.game_status.match_outcome == 3:
                match.radiant_win = False
            else:
                match.radiant_win = None

            self.players = {}
            for player in PlayerInMatch.query. \
                    options(joinedload_all('player')). \
                    filter(PlayerInMatch.match_id == self.job.match_id). \
                    all():
                if player.player.current_match == self.job.match_id:
                    player.player.current_match = None
                self.players[player.player_id] = player

            # Process scoreboard updates
            for player_id, player in self.players.items():
                score = Scoreboard.query.filter_by(ladder_name=match.section,
                                                   user_id=player_id).first()
                if score is None:
                    score = Scoreboard(user=player.player,
                                       ladder_name=match.section)
                    db.session.add(score)
                score.matches += 1
            for player in self.game_status.members:
                if player.id == self.dota.steam_id:
                    continue
                id = player.id
                score = Scoreboard.query.filter_by(ladder_name=match.section,
                                                   user_id=id).first()
                if (self.players[id].is_radiant and self.game_status.match_outcome == 2) or \
                        (not self.players[id].is_radiant and self.game_status.match_outcome == 3):
                    score.points += 1
                    score.win += 1
                elif (self.players[id].is_radiant and self.game_status.match_outcome == 3) or \
                        (not self.players[id].is_radiant and self.game_status.match_outcome == 2):
                    score.loss += 1
            for player in self.game_status.left_members:
                score = Scoreboard.query.filter_by(ladder_name=match.section,
                                                   user_id=player.id).first()
                self.players[player.id].is_leaver = True
                score.points -= 3
                score.leave += 1

            db.session.commit()
user_password = "******"

logging.basicConfig(format='[%(asctime)s] %(levelname)s %(name)s: %(message)s', level=logging.DEBUG)

client = SteamClient()
dota = Dota2Client(client)

login_called = False

@client.on('connected')
def login():
    global login_called
    if not login_called:
        client.login(user_name, user_password)
        login_called = True

@client.on(client.EVENT_AUTH_CODE_REQUIRED)
def auth_code_prompt(is_2fa, code_mismatch):
    if is_2fa:
        code = input("Enter 2FA Code: ")
        client.login(user_name, user_password, two_factor_code=code)
    else:
        code = input("Enter Email Code: ")
        client.login(user_name, user_password, auth_code=code)

@client.on('logged_on')
def start_dota():
    dota.launch()

client.connect()
client.run_forever()
Пример #3
0
class DotaBot(Greenlet):
    """A worker thread, connecting to steam to process a unique job.

    Attributes:
        worker_manager: `WorkerManager` this bot is linked to.
        credential: `Credential` used to connect to steam.
    """
    def __init__(self, worker_manager, credential, admins, casters, id, name,
                 password, team1, team2, team1_ids, team2_ids,
                 team_choosing_first):
        """Initialize the Dota bot thread for a unique job process.

        Args:
            worker_manager: `DazzarWorkerManager` this bot is linked to.
            credential: `Credential` used to connect to steam.
        """
        Greenlet.__init__(self)

        # High Level properties
        self.credential = credential
        self.worker_manager = worker_manager
        self.app = self.worker_manager.app

        self.client = SteamClient()
        self.dota = dota2.Dota2Client(self.client)

        # Game settings
        self.id = id
        self.name = name
        self.password = password
        self.team1 = team1
        self.team2 = team2
        self.team1_ids = team1_ids
        self.team2_ids = team2_ids
        self.admins = admins
        self.casters = casters
        self.lobby_options = {
            'game_name': self.name,
            'pass_key': self.password,
            'game_mode': dota2.enums.DOTA_GameMode.DOTA_GAMEMODE_CM,
            'server_region': int(dota2.enums.EServerRegion.Europe),
            'fill_with_bots': False,
            'allow_spectating': True,
            #'allow_cheats': False,
            'allow_cheats': True,
            'allchat': False,
            'dota_tv_delay': 2,
            'pause_setting': 1,
            #'leagueid': 4947 # FTV LEAGUE SEASON 1
            #'leagueid': 9674 # FTV LEAGUE SEASON 2
        }

        # Choices
        self.team_choosing_first = team_choosing_first
        self.team_choosing_now = team_choosing_first
        self.team_choices = [None, None]
        self.team_choices_possibilities = ['!radiant', '!dire', '!fp', '!sp']
        self.team_inverted = False

        # State machine variables
        self.machine_state = DotaBotState.STARTING
        self.lobby_status = {}
        """
        self.lobby_channel_id = None
        self.invite_timer = None
        self.missing_players = None
        self.missing_players_count = None
        self.wrong_team_players = None
        self.wrong_team_players_count = None
        """
        # Prepare all event handlers
        # - Steam client events
        # - Dota client events
        # - Bot events
        self.client.on('connected', self.steam_connected)
        self.client.on('logged_on', self.steam_logged)
        self.client.on('disconnected', self.steam_disconnected)

        self.dota.on('ready', self.dota_ready)
        self.dota.on('notready', self.closed_dota)

        self.dota.on(dota2.features.Lobby.EVENT_LOBBY_NEW, self.game_hosted)
        self.dota.on(dota2.features.Lobby.EVENT_LOBBY_CHANGED,
                     self.game_update)
        self.dota.channels.on(
            dota2.features.chat.ChannelManager.EVENT_JOINED_CHANNEL,
            self.channel_join)
        self.dota.channels.on(dota2.features.chat.ChannelManager.EVENT_MESSAGE,
                              self.channel_message)

    def _run(self):
        """Start the main loop of the thread, connecting to Steam, waiting for the job to finish to close the bot."""
        self.print_info('Connecting to Steam...')
        self.client.connect(retry=None)  # Try connecting with infinite retries

        while self.machine_state != DotaBotState.SETUP_GAME:
            sleep(5)
        self.initialize_lobby()
        sleep(10)  # Wait for setup

        remaining_time = 1800  # Attente 30 min au max

        # P1: Wait for people to join for 25 minutes, P2 if slots are filled (X=min remaining )
        self.print_info('Waiting for players.')
        team_names, missing_players = self.check_teams()
        while (remaining_time > 300
               and (team_names[0] is False or team_names[1] is False
                    or missing_players[0] != 0 or missing_players[1] != 0)):
            self.display_status(remaining_time, missing_players, team_names)
            sleep(30)
            remaining_time -= 30
            team_names, missing_players = self.check_teams()

        # P2: Give 1min for each teams to pick sides/picks
        self.print_info('Choice side/order.')
        self.machine_state = DotaBotState.PICKING_SIDE_ORDER
        msg = '{0} Choix du side/ordre, team {1} choisit entre {2}.'.format(
            self.remaining_time_to_string(remaining_time),
            self.team_choosing_now, ', '.join(self.team_choices_possibilities))
        self.dota.channels.lobby.send(msg)

        team_choice_remaining_time = [60, 60]
        while team_choice_remaining_time[self.team_choosing_now - 1]:
            sleep(1)
            remaining_time -= 1
            team_choice_remaining_time[self.team_choosing_now - 1] -= 1
            if self.team_choices[self.team_choosing_now - 1] is not None:
                break

        if self.team_choices[self.team_choosing_now - 1] is None:
            # RANDOM
            self.team_choices[self.team_choosing_now - 1] = random.choice(
                self.team_choices_possibilities)[1:]
            msg = '{0} Team {1} a random {2}.'.format(
                self.remaining_time_to_string(remaining_time),
                self.team_choosing_now,
                self.team_choices[self.team_choosing_now - 1])
        else:
            # Choice
            msg = '{0} Team {1} a choisi {2}.'.format(
                self.remaining_time_to_string(remaining_time),
                self.team_choosing_now,
                self.team_choices[self.team_choosing_now - 1])

        self.team_choices_possibilities.remove('!{0}'.format(
            self.team_choices[self.team_choosing_now - 1]))
        complementary = {
            'fp': '!sp',
            'sp': '!fp',
            'radiant': '!dire',
            'dire': '!radiant'
        }
        self.team_choices_possibilities.remove(
            complementary[self.team_choices[self.team_choosing_now - 1]])
        self.team_choosing_now = (self.team_choosing_now % 2) + 1
        msg = '{0} Team {1} choisit entre {2}.'.format(
            msg, self.team_choosing_now,
            ', '.join(self.team_choices_possibilities))
        self.dota.channels.lobby.send(msg)

        while team_choice_remaining_time[self.team_choosing_now - 1]:
            sleep(1)
            remaining_time -= 1
            team_choice_remaining_time[self.team_choosing_now - 1] -= 1
            if self.team_choices[self.team_choosing_now - 1] is not None:
                break

        if self.team_choices[self.team_choosing_now - 1] is None:
            # RANDOM
            self.team_choices[self.team_choosing_now - 1] = random.choice(
                self.team_choices_possibilities)[1:]
            msg = '{0} Team {1} a random {2}.'.format(
                self.remaining_time_to_string(remaining_time),
                self.team_choosing_now,
                self.team_choices[self.team_choosing_now - 1])
        else:
            # Choice
            msg = '{0} Team {1} a choisi {2}. Lancement dès que le lobby est complet.'.format(
                self.remaining_time_to_string(remaining_time),
                self.team_choosing_now,
                self.team_choices[self.team_choosing_now - 1])
        self.dota.channels.lobby.send(msg)

        # Config
        if self.team_choices[0] == 'dire' or self.team_choices[1] == 'radiant':
            self.team_inverted = True
            self.dota.flip_lobby_teams()
        if ((self.team_choices[0] == 'fp' and self.team_choices[1] == 'dire')
                or (self.team_choices[0] == 'sp'
                    and self.team_choices[1] == 'radiant')
                or (self.team_choices[0] == 'radiant'
                    and self.team_choices[1] == 'sp') or
            (self.team_choices[0] == 'dire' and self.team_choices[1] == 'fp')):
            self.lobby_options['cm_pick'] = DOTA_CM_PICK.DOTA_CM_GOOD_GUYS
        else:
            self.lobby_options['cm_pick'] = DOTA_CM_PICK.DOTA_CM_BAD_GUYS
        self.dota.config_practice_lobby(options=self.lobby_options)
        self.machine_state = DotaBotState.WAITING_FOR_READY

        # Resync time to modulo 30
        sleep(remaining_time % 30)
        remaining_time -= remaining_time % 30

        # P3: Give min(1, 30-X-2) min for teams to get into slots, starts when both teams ready
        team_names, missing_players = self.check_teams()
        while (remaining_time > 0
               and (team_names[0] is False or team_names[1] is False
                    or missing_players[0] != 0 or missing_players[1] != 0)):
            self.display_status(remaining_time, missing_players, team_names)
            sleep(30)
            remaining_time -= 30
            team_names, missing_players = self.check_teams()

        # Cancel lobby test
        if remaining_time <= 0:
            self.print_info('Lobby incomplet, annulation de la game.')
            self.dota.channels.lobby.send('Joueurs manquants, lobby annulé.')
            with self.app.app_context():
                game = db.session().query(Game).filter(
                    Game.id == self.id).one_or_none()
                if game is not None:
                    game.status = GameStatus.CANCELLED
                    db.session().commit()
            sleep(15)
            self.end_bot()

        # Start and retry
        self.dota.channels.lobby.send('Démarrage de la partie...')
        self.machine_state = DotaBotState.LOADING_GAME
        self.dota.launch_practice_lobby()
        while self.machine_state == DotaBotState.LOADING_GAME:
            sleep(5)

        if self.lobby_status.state == 0:
            self.print_info(
                'Erreur lors du chargement, annulation de la game.')
            self.dota.channels.lobby.send(
                'Impossible de charger la partie, game annulée. Contactez un admin.'
            )
            with self.app.app_context():
                game = db.session().query(Game).filter(
                    Game.id == self.id).one_or_none()
                if game is not None:
                    game.status = GameStatus.CANCELLED
                    db.session().commit()
            sleep(15)
            self.end_bot()

        # IN GAME WAIT
        self.print_info('Game in progress...')
        with self.app.app_context():
            game = db.session().query(Game).filter(
                Game.id == self.id).one_or_none()
            if game is not None:
                game.status = GameStatus.GAME_IN_PROGRESS
                db.session().commit()
        while self.lobby_status.state != 3:
            sleep(30)

        # Game over
        self.machine_state = DotaBotState.GAME_FINISHED
        self.print_info('Game completed.')
        with self.app.app_context():
            game = db.session().query(Game).filter(
                Game.id == self.id).one_or_none()
            if game is not None:
                game.status = GameStatus.COMPLETED
                game.valve_id = self.lobby_status.match_id
                if ((self.lobby_status.match_outcome == 2
                     and not self.team_inverted)
                        or (self.lobby_status.match_outcome == 3
                            and self.team_inverted)):
                    game.winner = 1
                else:
                    game.winner = 2
                db.session().commit()
        self.end_bot()

    def end_bot(self):
        """End the life of the bot."""
        self.print_info('Bot work over.')
        self.machine_state = DotaBotState.FINISHED

        self.dota.destroy_lobby()
        sleep(1)

        self.client.disconnect()
        self.worker_manager.bot_end(self.credential)
        self.kill()

    # Helpers

    def print_info(self, trace):
        """Wrapper of `logging.info` with bot name prefix.

        Args:
            trace: String to output as INFO.
        """
        logging.info('%s: %s', self.credential.login, trace)

    def print_error(self, trace):
        """Wrapper of `logging.error` with bot name prefix.

        Args:
            trace: String to output as ERROR.
        """
        logging.error("%s: %s", self.credential.login, trace)

    # Callback of Steam and Dota clients

    def steam_connected(self):
        """Callback fired when the bot is connected to Steam, login user."""
        self.print_info('Connected to Steam.')
        self.client.login(self.credential.login, self.credential.password)

    def steam_logged(self):
        """Callback fired when the bot is logged into Steam, starting Dota."""
        self.print_info('Logged to Steam.')
        self.dota.launch()

    def steam_disconnected(self):
        """Callback fired when the bot is disconnected from Steam"""
        self.print_info('Disconnected from Steam.')

    def dota_ready(self):
        """Callback fired when the Dota application is ready, resume the job processing."""
        self.print_info('Dota application is ready.')
        sleep(
            10
        )  # Safety to leave already existing lobby if bot is lost in the matrix
        self.host_game()

    def closed_dota(self):
        """Callback fired when the Dota application is closed."""
        self.print_info('Dota application is closed.')

    # Messaging events
    def channel_join(self, channel_info):
        pass
        #self.print_info('Channel join! {0}'.format(channel_info))
        """
        if channel_info.channel_type != dota2.enums.DOTAChatChannelType_t.DOTAChannelType_Lobby:
            self.dota.leave_channel(channel_info.channel_id)
        else:
            if self.game_status is not None:
                if channel_info.channel_name == 'Lobby_{0}'.format(self.game_status.lobby_id):
                    self.lobby_channel_id = channel_info.channel_id"""

    def channel_message(self, channel, message):
        if message.text[0] != '!':
            return
        command = message.text[1:].strip().split(' ')
        if len(command) == 0:
            return

        message_steam_id = SteamID(message.account_id).as_64
        # Feature commands
        if command[0] == 'commands':
            self.dota.channels.lobby.send(
                'Commandes: !cocaster, !destroy, !standin')
        elif command[0] == 'philaeux':
            self.dota.channels.lobby.send(
                'Respectez mon créateur ou je vous def lose.')
        elif command[0] == 'cocaster':
            if message_steam_id not in self.admins and message_steam_id not in self.casters:
                self.dota.channels.lobby.send(
                    'Seuls les casters et admins peuvent ajouter un cocaster.')
                return
            if len(command) != 2:
                self.dota.channels.lobby.send(
                    '!cocaster X où X est le steamID (64bits) du cocaster.')
                return
            if not command[1].isdigit():
                self.dota.channels.lobby.send(
                    'SteamID (64bits) invalide dans la commande !cocaster.')
            else:
                self.casters.append(int(command[1]))
                self.dota.channels.lobby.send('Cocaster {0} ajouté.'.format(
                    command[1]))
        elif command[0] == 'standin':
            if message_steam_id not in self.admins:
                self.dota.channels.lobby.send(
                    'Seuls les admins peuvent ajouter un standin.')
                return
            if len(command) != 3:
                self.dota.channels.lobby.send(
                    "!standin X Y où X est le steamID (64bits) du standin et Y l'équipe (1 ou 2)."
                )
                return
            if not command[1].isdigit():
                self.dota.channels.lobby.send(
                    'SteamID (64bits) invalide dans la commande !standin.')
                return
            if command[2] not in ['1', '2']:
                self.dota.channels.lobby.send(
                    'Équipe (1 ou 2) invalide dans la commande !standin.')
            else:
                if command[2] == '1':
                    self.team1_ids.append(int(command[1]))
                else:
                    self.team2_ids.append(int(command[1]))
                self.dota.channels.lobby.send(
                    "Standin {0} ajouté à l'équipe {1}.".format(
                        command[1], command[2]))
        elif command[0] == 'destroy':
            if message_steam_id not in self.admins:
                self.dota.channels.lobby.send(
                    'Seuls les admins peuvent détruirent le lobby.')
            else:
                self.dota.channels.lobby.send('Lobby annulé par un admin.')
                with self.app.app_context():
                    game = db.session().query(Game).filter(
                        Game.id == self.id).one_or_none()
                    if game is not None:
                        game.status = GameStatus.CANCELLED
                        db.session().commit()
                self.end_bot()

        # Main Loop commands
        elif command[0] == 'fp' or command[0] == 'sp' or command[
                0] == 'radiant' or command[0] == 'dire':
            if '!{0}'.format(command[0]) in self.team_choices_possibilities:
                if self.machine_state == DotaBotState.PICKING_SIDE_ORDER:
                    compare = self.team1_ids if self.team_choosing_now == 1 else self.team2_ids
                    if message_steam_id in compare:
                        self.team_choices[self.team_choosing_now -
                                          1] = command[0]

    # Hosting events
    def host_game(self):
        """Start the processing of the job with the appropriate handler."""
        self.print_info('Hosting game {0} with password {1}'.format(
            self.name, self.password))
        self.machine_state = DotaBotState.HOSTING_GAME
        self.dota.create_practice_lobby(password=self.password)

    def game_hosted(self, message):
        """Callback fired when the Dota bot enters a lobby."""
        if self.machine_state != DotaBotState.HOSTING_GAME:
            self.dota.destroy_lobby()
            self.dota.leave_practice_lobby(
            )  # Sometimes the bot get back to an old lobby at startup
            return

        self.lobby_status = message
        self.machine_state = DotaBotState.SETUP_GAME

    def game_update(self, message):
        """Callback fired when the game lobby change, update local information."""
        self.lobby_status = message
        if self.machine_state == DotaBotState.LOADING_GAME:
            if self.lobby_status.state == 0:
                self.machine_state = DotaBotState.RETRY_WAITING_FOR_READY
            elif self.lobby_status.state == 2:
                if self.lobby_status.game_state not in [0, 1]:
                    self.machine_state = DotaBotState.GAME_IN_PROGRESS

        # Kick players not authorized
        for member in message.members:
            if member.id == self.dota.steam_id:
                continue
            if (member.id not in self.team1_ids
                    and member.id not in self.team2_ids
                    and member.id not in self.admins
                    and member.id not in self.casters):
                self.dota.practice_lobby_kick(SteamID(member.id).as_32)
            if ((member.team == DOTA_GC_TEAM.SPECTATOR) or
                (member.team == DOTA_GC_TEAM.BROADCASTER and
                 not (member.id in self.admins or member.id in self.casters))):
                self.dota.practice_lobby_kick_from_team(
                    SteamID(member.id).as_32)
            else:
                if self.team_inverted:
                    if ((member.team == DOTA_GC_TEAM.BAD_GUYS
                         and member.id not in self.team1_ids)
                            or (member.team == DOTA_GC_TEAM.GOOD_GUYS
                                and member.id not in self.team2_ids)):
                        self.dota.practice_lobby_kick_from_team(
                            SteamID(member.id).as_32)
                else:
                    if ((member.team == DOTA_GC_TEAM.GOOD_GUYS
                         and member.id not in self.team1_ids)
                            or (member.team == DOTA_GC_TEAM.BAD_GUYS
                                and member.id not in self.team2_ids)):
                        self.dota.practice_lobby_kick_from_team(
                            SteamID(member.id).as_32)

    def initialize_lobby(self):
        """Setup the game lobby with the good options, and change status in database."""
        self.print_info('Game hosted, setup.')

        self.dota.channels.join_lobby_channel()
        self.dota.join_practice_lobby_team()
        self.dota.config_practice_lobby(options=self.lobby_options)
        with self.app.app_context():
            game = db.session().query(Game).filter(
                Game.id == self.id).one_or_none()
            if game is not None:
                game.status = GameStatus.WAITING_FOR_PLAYERS
                db.session().commit()
        self.machine_state = DotaBotState.WAITING_FOR_PLAYERS

    def check_teams(self):
        """Check team players and team names."""
        missing_players = [
            min(5, len(self.team1_ids)),
            min(5, len(self.team2_ids))
        ]
        team_names = [False, False]

        for member in self.lobby_status.members:
            if member.team == DOTA_GC_TEAM.GOOD_GUYS:
                missing_players[0] = missing_players[0] - 1
            elif member.team == DOTA_GC_TEAM.BAD_GUYS:
                missing_players[1] = missing_players[1] - 1
        i = 0
        if self.team_inverted:
            compare = [self.team2, self.team1]
        else:
            compare = [self.team1, self.team2]
        for team_detail in self.lobby_status.team_details:
            team_names[i] = team_detail.team_id == compare[i]
            i = i + 1
        return team_names, missing_players

    def display_status(self, remaining_time, missing_players, team_names):
        """Display status in chat."""
        msg = self.remaining_time_to_string(remaining_time)
        if not team_names[0]:
            msg = msg + " Le Radiant n'a pas la bonne team."
        if missing_players[0] == 1:
            msg = msg + " 1 joueur Radiant manquant."
        elif missing_players[0] > 1:
            msg = msg + " {0} joueurs Radiant manquants.".format(
                missing_players[0])
        if not team_names[1]:
            msg = msg + " Le Dire n'a pas la bonne team."
        if missing_players[1] == 1:
            msg = msg + " 1 joueur Dire manquant."
        elif missing_players[1] > 1:
            msg = msg + " {0} joueurs Dire manquants.".format(
                missing_players[1])
        self.dota.channels.lobby.send(msg)

    @staticmethod
    def remaining_time_to_string(remaining_time):
        return '{0}m{1:02d}s -'.format(remaining_time // 60,
                                       remaining_time % 60)