def farm_quests(self, quests: List[Quest] = None): """ Собирает награды из заданий. """ if quests is not None and not quests: logger.info('No quests to farm.') # provided but empty return logger.info('Farming quests…') self.log(f'✅ *{self.user.name}* выполняет задачи…') if quests is None: quests = self.api.get_all_quests() for quest in quests: if not quest.is_reward_available: continue if self.settings.bot.no_experience and quest.reward.experience: logger.warning( f'Ignoring {quest.reward.experience} experience reward for quest #{quest.id}.' ) continue with self.logger: self.logger.append(f'✅ *{self.user.name}* получает за задачу:', '') self.api.farm_quest(quest.id).log(self.logger) self.log(f'✅ *{self.user.name}* выполнил задачи.')
def get_arena_replays(self): """ Читает и сохраняет журналы арен. """ self.log(f'📒️ *{self.user.name}* читает журнал арены…') replays: List[Replay] = [ *self.api.get_battle_by_type(BattleType.ARENA), *self.api.get_battle_by_type(BattleType.GRAND), ] for replay in replays: if f'replays:{replay.id}' in self.db: continue self.db[f'replays:{replay.id}'] = { 'start_time': replay.start_time.timestamp(), 'win': replay.result.win, 'attackers': [hero.dict() for hero in replay.attackers.values()], 'defenders': [ hero.dict() for defenders in replay.defenders for hero in defenders.values() ], } logger.info(f'Saved #{replay.id}.') self.log(f'📒️ *{self.user.name}* прочитал журнал арены.')
def quack(self): """ Отладочная задача. """ logger.info('About to quack…') self.log(f'🐤 *{self.user.name}* собирается крякать…') sleep(5) logger.info('Quack!') self.log(f'🐤 Бот *{self.user.name}* сказал: «Кря!»') return now() + timedelta(seconds=15)
def attack_any_arena( self, *, n_heroes: int, make_solver: Callable[[Model, List[Hero]], ArenaSolver], attack: Callable[[ArenaSolution], Tuple[ArenaResult, Quests]], finalise: Callable[[], Any], ): self.log(f'⚔️ *{self.user.name}* идет на арену…') # Load arena model. logger.info('Loading model…') try: model: Model = pickle.loads(b85decode(self.db['bot:model'])) except KeyError: logger.warning('Model is not ready yet.') return logger.trace('Model: {}.', model) # Get all heroes. heroes = self.api.get_all_heroes() if len(heroes) < n_heroes: logger.warning('Not enough heroes: {} needed, you have {}.', n_heroes, len(heroes)) return # Refresh clan ID. self.user = self.api.get_user_info() # Pick an enemy and select attackers. solution = make_solver(model, heroes).solve() with self.logger: self.logger.append(f'⚔️ *{self.user.name}* атакует арену:', '') solution.log(self.logger) # Retry if win probability is too low. if solution.probability < constants.ARENA_MIN_PROBABILITY: logger.warning('Win probability is too low.') self.log(f'⚔️ *{self.user.name}* отменил атаку.') return now() + constants.ARENA_RETRY_INTERVAL # Attack! result, quests = attack(solution) # Collect results. with self.logger: self.logger.append(f'⚔️ *{self.user.name}* закончил арену:\n') result.log(self.logger) finalise() self.farm_quests(quests)
def farm_mail(self): """ Собирает награды из почты. """ logger.info('Farming mail…') self.log(f'📩 *{self.user.name}* читает почту…') if letters := self.api.get_all_mail(): logger.info(f'{len(letters)} letters.') with self.logger: self.logger.append(f'📩 *{self.user.name}* получил из почты:\n') log_rewards( self.api.farm_mail(letter.id for letter in letters).values(), self.logger)
def main(settings: Settings, verbosity: int, shell: bool): """ Hero Wars game bot ЪЈє """ install_logging(verbosity) logger.info('Bot is starting.') with Session() as session, Database(constants.DATABASE_NAME) as db: session.mount('https://', HTTPAdapter(max_retries=5)) session.headers['User-Agent'] = constants.USER_AGENT telegram = Telegram(session, settings.telegram) if settings.telegram else None api = API(session, db, settings) bot = Bot(db, api, VK(session, settings), telegram, settings) bot.log('ЪјЅ лЉлЙЛѓ лил░л┐ЛЃЛЂл║л░лхЛѓЛЂЛЈРђд') api.prepare() bot.prepare() logger.info('Welcome ┬Ф{}┬╗!', bot.user.name) logger.info('Game time: {:%H:%M:%S %Z}', datetime.now(bot.user.tz)) logger.info('Next day: {:%H:%M:%S %Z}.', bot.user.next_day.astimezone(bot.user.tz)) bot.log(f'ЪјЅ *{bot.user.name}* лил░л┐ЛЃЛЂЛѓлИл╗ЛЂЛЈ!') if not shell: bot.run() else: IPython.embed()
def get_raid_mission_ids(self) -> Iterable[str]: missions: Dict[str, Mission] = { mission.id: mission for mission in self.api.get_all_missions() if mission.is_raid_available and mission_name(mission.id).lower() in self.settings.bot.raid_missions } # Get heroic mission IDs. heroic_mission_ids = get_heroic_mission_ids() # First, yield heroic missions. raided_heroic_mission_ids = list(missions.keys() & heroic_mission_ids) shuffle(raided_heroic_mission_ids ) # shuffle in order to distribute stamina evenly logger.info(f'Raided heroic missions: {raided_heroic_mission_ids}.') for mission_id in raided_heroic_mission_ids: tries_left = constants.RAID_N_HEROIC_TRIES - missions[ mission_id].tries_spent logger.info(f'Mission #{mission_id}: {tries_left} tries left.') for _ in range(tries_left): yield mission_id # Then, randomly choose non-heroic missions infinitely. non_heroic_mission_ids = list(missions.keys() - heroic_mission_ids) logger.info(f'Raided non-heroic missions: {non_heroic_mission_ids}.') if not non_heroic_mission_ids: logger.info('No raided non-heroic missions.') return while True: yield choice(non_heroic_mission_ids)
def level_up_titan_hero_gift(self): """ Вложить и сбросить искры самому слабому герою. """ self.log(f'⚡️ *{self.user.name}* вкладывает и сбрасывает искры мощи…') hero = min(self.api.get_all_heroes(), key=attrgetter('power')) logger.info('Hero: {}.', hero) self.farm_quests(self.api.level_up_titan_hero_gift(hero.id)) reward, quests = self.api.drop_titan_hero_gift(hero.id) with self.logger: self.logger.append( f'⚡️ *{self.user.name}* получил за вложение искр мощи:', '') reward.log(self.logger) self.farm_quests(quests) self.log(f'⚡️ *{self.user.name}* вложил и сбросил искры мощи.')
def raid_bosses(self): """ Рейдит боссов Запределья. """ self.log(f'🔴 *{self.user.name}* рейдит боссов Запределья…') for i, boss in enumerate(self.api.get_all_bosses(), 1): self.log(f'🔴 *{self.user.name}* рейдит боссов Запределья: {i}-й…') if boss.may_raid: logger.info(f'Raid boss #{boss.id}…') self.api.raid_boss(boss.id).log() rewards, quests = self.api.open_boss_chest(boss.id) with self.logger: self.logger.append( f'🔴 *{self.user.name}* получил в Запределье:', '') log_rewards(rewards, self.logger) self.farm_quests(quests) else: logger.info(f'May not raid boss #{boss.id}.') self.log(f'🔴 *{self.user.name}* закончил рейд боссов Запределья.')
def open_titan_artifact_chest(self): """ Открывает сферы артефактов титанов. """ self.log(f'⚫️ *{self.user.name}* открывает сферы артефактов титанов…') for amount in [10, 1]: try: rewards, quests = self.api.open_titan_artifact_chest(amount) except NotEnoughError: logger.info(f'Not enough resources to open {amount} chests.') else: with self.logger: self.logger.append( f'⚫️ *{self.user.name}* получил из сферы артефактов титанов:', '') log_rewards(rewards, self.logger) self.farm_quests(quests) break self.log(f'⚫️ *{self.user.name}* открыл сферы артефактов титанов.')
def raid_missions(self): """ Ходит в рейды в миссиях в кампании за предметами. """ self.log(f'🔥 *{self.user.name}* идет в рейды…') for mission_id in self.get_raid_mission_ids(): logger.info( f'Raid mission #{mission_id} «{mission_name(mission_id)}»…') try: with self.logger: rewards = self.api.raid_mission(mission_id) self.logger.append( f'🔥 *{self.user.name}* получил из рейда «{mission_name(mission_id)}»:', '') log_rewards(rewards, self.logger) except NotEnoughError as e: logger.info(f'Not enough: {e}.') break self.log(f'🔥 *{self.user.name}* сходил в рейды.')
def check_freebie(self): """ Собирает подарки на странице игры ВКонтакте. """ self.log(f'🎁 *{self.user.name}* проверяет подарки на VK.com…') should_farm_mail = False for gift_id in self.vk.find_gifts(): if f'gifts:{self.api.user_id}:{gift_id}' in self.db: continue logger.info(f'Checking {gift_id}…') reward = self.api.check_freebie(gift_id) if reward is not None: reward.log() should_farm_mail = True self.db[f'gifts:{self.api.user_id}:{gift_id}'] = True self.log(f'🎁 *{self.user.name}* проверил подарки на VK.com.') if should_farm_mail: self.farm_mail()
def shop(self): """ Покупает в магазине вещи. """ self.log(f'🛍 *{self.user.name}* идет в магазин…') logger.info('Requesting shops…') slots: List[Tuple[str, str]] = [ (shop_id, slot.id) for shop_id in constants.SHOP_IDS for slot in self.api.get_shop(shop_id) if (not slot.is_bought) and (not slot.cost.star_money) and ( slot.reward.keywords & self.settings.bot.shops) ] logger.info(f'Going to buy {len(slots)} slots.') for shop_id, slot_id in slots: logger.info( f'Buying slot #{slot_id} in shop «{shop_name(shop_id)}»…') try: reward = self.api.shop(shop_id=shop_id, slot_id=slot_id) except NotEnoughError as e: logger.warning(f'Not enough: {e}') except AlreadyError as e: logger.warning(f'Already: {e}') else: with self.logger: self.logger.append(f'🛍 *{self.user.name}* купил:', '') reward.log(self.logger) self.log(f'🛍 *{self.user.name}* сходил в магазин.')
def farm_zeppelin_gift(self): """ Собирает ключ у валькирии и открывает артефактные сундуки. """ self.log(f'🔑 *{self.user.name}* открывает артефактные сундуки…') self.api.farm_zeppelin_gift().log() for _ in range(constants.MAX_OPEN_ARTIFACT_CHESTS): try: rewards = self.api.open_artifact_chest() except NotEnoughError: logger.info('All keys are spent.') break with self.logger: self.logger.append( f'🔑 *{self.user.name}* получил из артефактного сундука:', '') log_rewards(rewards, self.logger) else: logger.warning('Maximum number of chests opened.') self.log(f'🔑 *{self.user.name}* открыл артефактные сундуки.')
def hall_of_fame(self): """ Турнир Стихий. """ self.log(f'💨 *{self.user.name}* идет в Турнир Стихий…') weekday = now().weekday() if weekday == calendar.SATURDAY: logger.info('Farming reward today…') trophy = self.api.get_hall_of_fame().trophy if trophy: reward = self.api.farm_hall_of_fame_trophy_reward(trophy.week) with self.logger: self.logger.append( f'💨 *{self.user.name}* получил в Турнире Стихий:\n') reward.log(self.logger) else: logger.warning('No trophy.') else: logger.info('Doing nothing today.') self.log(f'💨 *{self.user.name}* закончил Турнир Стихий.')
def skip_tower(self): """ Зачистка башни. """ self.log(f'🗼 *{self.user.name}* проходит башню…') tower = self.api.get_tower_info() # Yeah, it's a bit complicated… while tower.floor_number <= 50: logger.info(f'Floor #{tower.floor_number}: {tower.floor_type}.') self.log( f'🗼 *{self.user.name}* на {tower.floor_number}-м этаже башни…') if tower.floor_type == TowerFloorType.BATTLE: # If we have the topmost level, then we can skip the tower entirely. # But we need to go chest by chest. So go to the next chest. if tower.may_full_skip: tower = self.api.next_tower_chest() # Maybe we can skip the floor, because of the yesterday progress. elif tower.floor_number <= tower.may_skip_floor: tower, reward = self.api.skip_tower_floor() reward.log() # Otherwise, we're not able to continue. else: break elif tower.floor_type == TowerFloorType.CHEST: # The simplest one. Just open a random chest. reward, _ = self.api.open_tower_chest(choice([0, 1, 2])) with self.logger: self.logger.append( f'🗼 *{self.user.name}* получил на {tower.floor_number}-м этаже башни:\n' ) reward.log(self.logger) # If it was the topmost floor, we have to stop. if tower.floor_number == 50: logger.success('Finished. It was the topmost floor.') break # If we can skip the tower entirely, then go to the next chest. if tower.may_full_skip: tower = self.api.next_tower_chest() # Otherwise, just proceed to the next floor. else: tower = self.api.next_tower_floor() elif tower.floor_type == TowerFloorType.BUFF: # Buffs go from the cheapest to the most expensive. # So try to buy the most expensive ones first. for buff in reversed(tower.floor): buff_id = int(buff['id']) # Some buffs require to choose a hero. We ignore these. if buff_id not in constants.TOWER_IGNORED_BUFF_IDS: try: self.api.buy_tower_buff(buff_id) except NotEnoughError: logger.info( f'Not enough resources for buff #{buff_id}.') except AlreadyError: logger.info(f'Already bought buff #{buff_id}.') except NotFoundError as e: logger.warning( f'Not found for buff #{buff_id}: {e}.') else: logger.debug(f'Skip buff #{buff_id}.') # Then normally proceed to the next floor. tower = self.api.next_tower_floor() self.log( f'🗼 *{self.user.name}* закончил башню на *{tower.floor_number}-м* этаже.' )
def send_expeditions(self) -> Optional[datetime]: logger.info('Sending expeditions…') self.log(f'⛺️ *{self.user.name}* пробует отправить экспедиции…') # Need to know which expeditions are already started. expeditions = self.api.list_expeditions() started_expeditions = [ expedition for expedition in expeditions if expedition.is_started ] logger.info('{} expeditions in progress.', len(started_expeditions)) next_run_at = min( [expedition.end_time for expedition in started_expeditions], default=None) if next_run_at: logger.info('The earliest expedition finishes at {}.', next_run_at.astimezone(self.user.tz)) # Select available heroes. busy_ids = { hero_id for expedition in started_expeditions for hero_id in expedition.hero_ids } logger.info('Busy heroes: {}.', busy_ids) heroes: Dict[str, Hero] = { hero.id: hero for hero in self.api.get_all_heroes() if hero.id not in busy_ids } logger.info('{} heroes are still available.', len(heroes)) # Let's see which expeditions are available. available_expeditions = [ expedition for expedition in expeditions if expedition.is_available ] logger.info('{} expeditions are still available.', len(available_expeditions)) while available_expeditions: # Choose the least powerful expedition. expedition, *available_expeditions = sorted( available_expeditions, key=attrgetter('power')) logger.info('The optimal expedition power is {}.', expedition.power) # Choose the least powerful appropriate team. team = find_expedition_team(heroes.values(), expedition.power) if team is None: logger.info('Could not find powerful enough team.') break # Send the expedition. end_time, quests = self.api.send_expedition_heroes( expedition.id, get_unit_ids(team)) self.log( f'⛺️ *{self.user.name}* отправил экспедицию #{expedition.id}.') self.farm_quests(quests) # Exclude the busy heroes. for hero in team: del heroes[hero.id] # We should farm the earliest finished expedition. if next_run_at is None or end_time < next_run_at: next_run_at = end_time self.log(f'⛺️ *{self.user.name}* закончил с отправкой экспедиций.') return next_run_at