def __init__(self, name: str, level: int): self.name = normalize_name(name) self.base_name = self.name self.level = level self.base_stats = pokedex[self.name][constants.BASESTATS] self.stats = calculate_stats(self.base_stats, self.level) self.max_hp = self.stats.pop(constants.HITPOINTS) self.hp = self.max_hp if self.name == 'shedinja': self.max_hp = 1 self.hp = 1 self.ability = normalize_name( pokedex[self.name][constants.MOST_LIKELY_ABILITY]) self.types = pokedex[self.name][constants.TYPES] self.item = 'unknown' self.fainted = False self.moves = [] self.status = None self.volatile_statuses = [] self.boosts = defaultdict(lambda: 0) self.can_mega_evo = False self.can_ultra_burst = True
def start_volatile_status(battle, split_msg): if is_opponent(battle, split_msg): pkmn = battle.opponent.active else: pkmn = battle.user.active volatile_status = normalize_name(split_msg[3].split(":")[-1]) if volatile_status not in pkmn.volatile_statuses: logger.debug("Starting the volatile status {} on {}".format( volatile_status, pkmn.name)) pkmn.volatile_statuses.append(volatile_status) if volatile_status == constants.DYNAMAX: pkmn.hp *= 2 pkmn.max_hp *= 2 logger.debug("{} started dynamax - doubling their HP to {}/{}".format( pkmn.name, pkmn.hp, pkmn.max_hp)) if constants.ABILITY in split_msg[3]: pkmn.ability = volatile_status if len(split_msg) == 6 and constants.ABILITY in normalize_name( split_msg[5]): pkmn.ability = normalize_name(split_msg[5].split('ability:')[-1]) if volatile_status == constants.TYPECHANGE: new_type = normalize_name(split_msg[4]) logger.debug("Setting {}'s type to {}".format(pkmn.name, new_type)) pkmn.types = [new_type]
def get_new_spreads(smogon_stats_url): """Parses a Smogon stats document, such as: 'https://www.smogon.com/stats/2019-02/moveset/gen7ou-1825.txt' Returns a dictionary containing the most common spreads for the pokemon in the file """ r = requests.get(smogon_stats_url) split_string = str(r.content).split(new_pokemon_indicator) spreads = dict() for pokemon_data in split_string: segments = pokemon_data.split('|') it = iter(segments) pokemon_name = normalize_name(segments[1]) spreads[pokemon_name] = list() for segment in it: if normalize_name(segment) == spreads_string: while section_end_string not in segment: segment = next(it) if ':' in segment: split_segment = segment.split() spread = split_segment[0] nature = normalize_name(spread.split(':')[0]) evs = spread.split(':')[1].replace('/', ',') spreads[pokemon_name].append((nature, evs)) return spreads
def heal_or_damage(battle, split_msg): """The opponent's active pokemon has healed""" if is_opponent(battle, split_msg): pkmn = battle.opponent.active # opponent hp is given as a percentage if constants.FNT in split_msg[3]: pkmn.hp = 0 else: new_hp_percentage = float(split_msg[3].split('/')[0]) / 100 pkmn.hp = pkmn.max_hp * new_hp_percentage if len(split_msg) == 5 and constants.TOXIC in split_msg[ 3] and '[from] psn' in split_msg[4]: battle.opponent.side_conditions[constants.TOXIC_COUNT] += 1 # give that pokemon an item if this string specifies one if len(split_msg) == 5 and constants.ITEM in split_msg[4]: item = normalize_name(split_msg[4].split( constants.ITEM)[-1].strip(": ")) logger.debug("Setting opponent's item to: {}".format(item)) pkmn.item = item # set the ability if the information is shown if len(split_msg) >= 5 and constants.ABILITY in split_msg[4]: ability = normalize_name(split_msg[4].split( constants.ABILITY)[-1].strip(": ")) logger.debug("Setting opponent's item ability {}".format(ability)) pkmn.ability = ability else: if len(split_msg) == 5 and constants.TOXIC in split_msg[ 3] and '[from] psn' in split_msg[4]: battle.user.side_conditions[constants.TOXIC_COUNT] += 1
def set_opponent_ability(battle, split_msg): if is_opponent(battle, split_msg): for msg in split_msg: if constants.ABILITY in normalize_name(msg): ability = normalize_name(msg.split(':')[-1]) logger.debug("Setting opponent ability to {}".format(ability)) battle.opponent.active.ability = ability
def heal_or_damage(battle, split_msg): if is_opponent(battle, split_msg): side = battle.opponent other_side = battle.user pkmn = battle.opponent.active # opponent hp is given as a percentage if constants.FNT in split_msg[3]: pkmn.hp = 0 else: new_hp_percentage = float(split_msg[3].split('/')[0]) / 100 pkmn.hp = pkmn.max_hp * new_hp_percentage else: side = battle.user other_side = battle.opponent pkmn = battle.user.active if constants.FNT in split_msg[3]: pkmn.hp = 0 else: pkmn.hp = float(split_msg[3].split('/')[0]) pkmn.max_hp = float(split_msg[3].split('/')[1].split()[0]) # increase the amount of turns toxic has been active if len(split_msg) == 5 and constants.TOXIC in split_msg[ 3] and '[from] psn' in split_msg[4]: side.side_conditions[constants.TOXIC_COUNT] += 1 if len(split_msg) == 6 and split_msg[4].startswith( '[from] item:') and other_side.name in split_msg[5]: item = normalize_name(split_msg[4].split('item:')[-1]) logger.debug("Setting {}'s item to: {}".format(other_side.active.name, item)) other_side.active.item = item # set the ability for the other side (the side not taking damage, '-damage' only) if len(split_msg) == 6 and split_msg[4].startswith( '[from] ability:' ) and other_side.name in split_msg[5] and split_msg[1] == '-damage': ability = normalize_name(split_msg[4].split('ability:')[-1]) logger.debug("Setting {}'s ability to: {}".format( other_side.active.name, ability)) other_side.active.ability = ability # set the ability of the side (the side being healed, '-heal' only) if len(split_msg) == 6 and constants.ABILITY in split_msg[ 4] and other_side.name in split_msg[5] and split_msg[1] == '-heal': ability = normalize_name(split_msg[4].split( constants.ABILITY)[-1].strip(": ")) logger.debug("Setting {}'s ability to: {}".format(pkmn.name, ability)) pkmn.ability = ability # give that pokemon an item if this string specifies one if len(split_msg ) == 5 and constants.ITEM in split_msg[4] and pkmn.item is not None: item = normalize_name(split_msg[4].split( constants.ITEM)[-1].strip(": ")) logger.debug("Setting {}'s item to: {}".format(pkmn.name, item)) pkmn.item = item
def move(battle, split_msg): move_name = normalize_name(split_msg[3].strip().lower()) if is_opponent(battle, split_msg): side = battle.opponent pkmn = battle.opponent.active else: side = battle.user pkmn = battle.user.active # add the move to it's moves if it hasn't been seen # decrement the PP by one # if the move is unknown, do nothing move_object = pkmn.get_move(move_name) if move_object is None: new_move = pkmn.add_move(move_name) if new_move is not None: new_move.current_pp -= 1 else: move_object.current_pp -= 1 logger.debug("{} already has the move {}. Decrementing the PP by 1".format(pkmn.name, move_name)) # if this pokemon used two different moves without switching, # set a flag to signify that it cannot have a choice item if ( is_opponent(battle, split_msg) and side.last_used_move.pokemon_name == side.active.name and side.last_used_move.move != move_name ): logger.debug("{} used two different moves - it cannot have a choice item".format(pkmn.name)) pkmn.can_have_choice_item = False if pkmn.item in constants.CHOICE_ITEMS: logger.warning("{} has a choice item, but used two different moves - setting it's item to UNKNOWN".format(pkmn.name)) pkmn.item = constants.UNKNOWN_ITEM try: category = all_move_json[move_name][constants.CATEGORY] logger.debug("Setting {}'s last used move: {}".format(pkmn.name, move_name)) side.last_used_move = LastUsedMove( pokemon_name=pkmn.name, move=move_name ) except KeyError: category = None side.last_used_move = LastUsedMove( pokemon_name=pkmn.name, move=constants.DO_NOTHING_MOVE ) # if this pokemon used a damaging move, eliminate the possibility of it having a lifeorb # the lifeorb will reveal itself if it has it if category in constants.DAMAGING_CATEGORIES and not any([normalize_name(a) in ['sheerforce', 'magicguard'] for a in pokedex[pkmn.name][constants.ABILITIES].values()]): logger.debug("{} used a damaging move - not guessing lifeorb anymore".format(pkmn.name)) pkmn.can_have_life_orb = False # there is nothing special in the protocol for "wish" - it must be extracted here if move_name == constants.WISH and 'still' not in split_msg[4]: logger.debug("{} used wish - expecting {} health of recovery next turn".format(side.active.name, side.active.max_hp/2)) side.wish = (2, side.active.max_hp/2)
def get_move_information(m): try: return m.split('|')[2], all_move_json[normalize_name( m.split('|')[3])] except KeyError: logger.debug( "Unknown move {} - using standard 0 priority move".format( normalize_name(m.split('|')[3]))) return m.split('|')[2], {constants.PRIORITY: 0}
def weather(battle, split_msg): """Set the battle's weather""" weather_name = normalize_name(split_msg[2].split(':')[-1].strip()) logger.debug("Weather {} started".format(weather_name)) battle.weather = weather_name if len(split_msg) >= 5 and battle.opponent.name in split_msg[4]: ability = normalize_name(split_msg[3].split(':')[-1].strip()) logger.debug("Setting opponent ability to {}".format(ability)) battle.opponent.active.ability = ability
def set_ability(battle, split_msg): if is_opponent(battle, split_msg): side = battle.opponent else: side = battle.user for msg in split_msg: if constants.ABILITY in normalize_name(msg): ability = normalize_name(msg.split(':')[-1]) logger.debug("Setting {}'s ability to {}".format(side.active.name, ability)) side.active.ability = ability
def single_pokemon_export_to_dict(pkmn_export_string): def get_species(s): if '(' in s and ')' in s: species = s[s.find("(") + 1:s.find(")")] return species return None pkmn_dict = { "name": "", "species": "", "level": "", "gender": "", "item": "", "ability": "", "moves": [], "nature": "", "evs": { "hp": "", "atk": "", "def": "", "spa": "", "spd": "", "spe": "", }, } pkmn_info = pkmn_export_string.split('\n') name = pkmn_info[0].split('@')[0] if "(M)" in name: pkmn_dict["gender"] = "M" name = name.replace('(M)', '') if "(F)" in name: pkmn_dict["gender"] = "F" name = name.replace('(F)', '') species = get_species(name) if species: pkmn_dict["species"] = normalize_name(species) pkmn_dict["name"] = name.split('(')[0].strip() else: pkmn_dict["name"] = normalize_name(name.strip()) if '@' in pkmn_info[0]: pkmn_dict["item"] = normalize_name(pkmn_info[0].split('@')[1]) for line in pkmn_info[1:]: if line.startswith('Ability: '): pkmn_dict["ability"] = normalize_name(line.split('Ability: ')[-1]) elif line.startswith('Level: '): pkmn_dict["level"] = normalize_name(line.split('Level: ')[-1]) elif line.startswith('EVs: '): evs = line.split('EVs: ')[-1] for ev in evs.split('/'): ev = ev.strip() amount = normalize_name(ev.split(' ')[0]) stat = normalize_name(ev.split(' ')[1]) pkmn_dict['evs'][stat] = amount elif line.endswith('Nature'): pkmn_dict["nature"] = normalize_name(line.split('Nature')[0]) elif line.startswith('-'): pkmn_dict["moves"].append(normalize_name(line[1:])) return pkmn_dict
def weather(battle, split_msg): if is_opponent(battle, split_msg): side = battle.opponent else: side = battle.user weather_name = normalize_name(split_msg[2].split(':')[-1].strip()) logger.debug("Weather {} started".format(weather_name)) battle.weather = weather_name if len(split_msg) >= 5 and side.name in split_msg[4]: ability = normalize_name(split_msg[3].split(':')[-1].strip()) logger.debug("Setting {} ability to {}".format(side.active.name, ability)) side.active.ability = ability
def add_move(self, move_name: str): if normalize_name(move_name) in [m.name for m in self.moves]: return try: self.moves.append(Move(move_name)) except KeyError: logger.warning("{} is not a known move".format(move_name))
def __init__(self, name: str, level: int): self.name = normalize_name(name) self.base_name = self.name self.level = level try: self.base_stats = pokedex[self.name][constants.BASESTATS] except KeyError: logger.info("Could not pokedex entry for {}".format(self.name)) self.name = [k for k in pokedex if self.name.startswith(k)][0] logger.info("Using {} instead".format(self.name)) self.base_stats = pokedex[self.name][constants.BASESTATS] self.stats = calculate_stats(self.base_stats, self.level) self.max_hp = self.stats.pop(constants.HITPOINTS) self.hp = self.max_hp if self.name == 'shedinja': self.max_hp = 1 self.hp = 1 self.ability = None self.types = pokedex[self.name][constants.TYPES] self.item = constants.UNKNOWN_ITEM self.fainted = False self.moves = [] self.status = None self.volatile_statuses = [] self.boosts = defaultdict(lambda: 0) self.can_mega_evo = False self.can_ultra_burst = False self.is_mega = False self.can_have_choice_item = True
def transform(battle, split_msg): if is_opponent(battle, split_msg): transformed_into_name = normalize_name(split_msg[3].split(':')[1]) battle_copy = deepcopy(battle) battle.opponent.active.boosts = deepcopy(battle.user.active.boosts) battle_copy.user.from_json(battle_copy.request_json) if battle_copy.user.active.name == transformed_into_name or battle_copy.user.active.name.startswith( transformed_into_name): transformed_into = battle_copy.user.active else: transformed_into = find_pokemon_in_reserves( transformed_into_name, battle_copy.user.reserve) logger.debug("Opponent {} transformed into {}".format( battle.opponent.active.name, battle.user.active.name)) battle.opponent.active.stats = deepcopy(transformed_into.stats) battle.opponent.active.ability = deepcopy(transformed_into.ability) battle.opponent.active.moves = deepcopy(transformed_into.moves) battle.opponent.active.types = deepcopy(transformed_into.types) if constants.TRANSFORM not in battle.opponent.active.volatile_statuses: battle.opponent.active.volatile_statuses.append( constants.TRANSFORM)
def move(battle, split_msg): """The opponent's pokemon has made a move - add it to that pokemon's move list if necessary""" move_name = normalize_name(split_msg[3].strip().lower()) if is_opponent(battle, split_msg): pkmn = battle.opponent.active move = pkmn.get_move(move_name) if move is None: new_move = pkmn.add_move(move_name) if new_move is not None: new_move.current_pp -= 1 else: move.current_pp -= 1 logger.debug( "{} already has the move {}. Decrementing the PP by 1".format( pkmn.name, move_name)) logger.debug("Setting opponent's last used move: {} - {}".format( battle.opponent.active.name, move_name)) # if this pokemon used two different moves without switching, # set a flag to signify that it cannot have a choice item if (battle.opponent.last_used_move.pokemon_name == battle.opponent.active.name and battle.opponent.last_used_move.move != move_name): logger.debug( "Opponent's {} used two different moves - it cannot have a choice item" .format(battle.opponent.active.name)) battle.opponent.active.can_have_choice_item = False battle.opponent.last_used_move = LastUsedMove( pokemon_name=battle.opponent.active.name, move=move_name)
def get_damage_dealt(battle, split_msg, next_messages): move_name = normalize_name(split_msg[3]) critical_hit = False if is_opponent(battle, split_msg): attacking_side = battle.opponent defending_side = battle.user else: attacking_side = battle.user defending_side = battle.opponent for line in next_messages: next_line_split = line.split('|') # if one of these strings appears in index 1 then # exit out since we are done with this pokemon's move if len(next_line_split) < 2 or next_line_split[1] in MOVE_END_STRINGS: break elif next_line_split[1] == '-crit': critical_hit = True # if '-damage' appears, we want to parse the percentage damage dealt elif next_line_split[1] == '-damage' and defending_side.name in next_line_split[2]: final_health, maxhp, _ = get_pokemon_info_from_condition(next_line_split[3]) # maxhp can be 0 if the targetted pokemon fainted # the message would be: "0 fnt" if maxhp == 0: maxhp = defending_side.active.max_hp damage_dealt = (defending_side.active.hp / defending_side.active.max_hp)*maxhp - final_health damage_percentage = round(damage_dealt / maxhp, 4) logger.debug("{} did {}% damage to {} with {}".format(attacking_side.active.name, damage_percentage * 100, defending_side.active.name, move_name)) return DamageDealt(attacker=attacking_side.active.name, defender=defending_side.active.name, move=move_name, percent_damage=damage_percentage, crit=critical_hit)
def set_opponent_ability_from_ability_tag(battle, split_msg): if is_opponent(battle, split_msg): side = battle.opponent else: side = battle.user ability = normalize_name(split_msg[3]) logger.debug("Setting {}'s ability to {}".format(side.active.name, ability)) side.active.ability = ability
def __init__(self, name): name = normalize_name(name) move_json = all_move_json[name] self.name = name self.max_pp = int(move_json.get(constants.PP) * 1.6) self.disabled = False self.can_z = False self.current_pp = self.max_pp
def curestatus(battle, split_msg): if is_opponent(battle, split_msg): pkmn_name = split_msg[2].split(':')[-1].strip() if normalize_name(pkmn_name) == battle.opponent.active.name: pkmn = battle.opponent.active else: try: pkmn = next( filter(lambda x: x.name == normalize_name(pkmn_name), battle.opponent.reserve)) except StopIteration: logger.warning( "The pokemon {} does not exist in the opponent's party, defaulting to the active pokemon" .format(normalize_name(pkmn_name))) pkmn = battle.opponent.active pkmn.status = None
def status(battle, split_msg): if is_opponent(battle, split_msg): status_name = split_msg[3].strip() logger.debug("Opponent got status {}".format(status_name)) battle.opponent.active.status = status_name if len(split_msg) > 4 and 'item: ' in split_msg[4]: battle.opponent.active.item = normalize_name( split_msg[4].split('item:')[-1])
def __init__(self, name: str, level: int): # Single Number, Should be 1-hot (1136) self.name = normalize_name(name) self.base_name = self.name # Single Number, ignore the level because we assume Lv100 # if it's not level 100, it would be encoded in name (e.g. Rattata) self.level = level try: self.base_stats = pokedex[self.name][constants.BASESTATS] except KeyError: logger.info("Could not pokedex entry for {}".format(self.name)) self.name = [k for k in pokedex if self.name.startswith(k)][0] logger.info("Using {} instead".format(self.name)) self.base_stats = pokedex[self.name][constants.BASESTATS] # 6 numbers self.stats = calculate_stats(self.base_stats, self.level) # can ignore max_hp because it's noisy (hp% is pretty much all we care/know) self.max_hp = self.stats.pop(constants.HITPOINTS) # 1 number as percent self.hp = self.max_hp if self.name == 'shedinja': self.max_hp = 1 self.hp = 1 # TODO: Encode, currently not neccessary self.ability = None # Number between 1-17 self.types = pokedex[self.name][constants.TYPES] # should be ignored in the vector since we have no list of items available # closest item encoding we have is in that boolean below for life orb / choice self.item = constants.UNKNOWN_ITEM # Single number (0,1) self.fainted = False # for now, ignore. maybe one-hot encode moves later? self.moves = [] # one hot encoding for all statuses (0 or 1) # burn, freeze, sleep, paralysis, poison, badly poison (6 total) # badly poison ranges from 0-6, since there are poison stages self.status = None # 5 total volatile statuses # confusion, leech seed, substitute, taunt, partially trapped, transformed self.volatile_statuses = [] # 7 stat boost numbers (-6 to 6 for each stat) self.boosts = defaultdict(lambda: 0) # ignore self.can_mega_evo = False self.can_ultra_burst = False self.can_dynamax = False self.is_mega = False # either choice or life orb, so these are opposing or both 0 self.can_have_choice_item = True self.can_have_life_orb = True
def activate(battle, split_msg): if is_opponent(battle, split_msg): pkmn = battle.opponent.active else: pkmn = battle.user.active if split_msg[3].lower() == 'move: poltergeist': item = normalize_name(split_msg[4]) logger.debug("{} has the item {}".format(pkmn.name, item)) pkmn.item = item
def set_item(battle, split_msg): """Set the opponent's item""" if is_opponent(battle, split_msg): side = battle.opponent else: side = battle.user item = normalize_name(split_msg[3].strip()) logger.debug("Setting {}'s item to {}".format(side.active.name, item)) side.active.item = item
def check_choicescarf(battle, msg_lines): def get_move_information(m): try: return m.split('|')[2], all_move_json[normalize_name( m.split('|')[3])] except KeyError: logger.debug( "Unknown move {} - using standard 0 priority move".format( normalize_name(m.split('|')[3]))) return m.split('|')[2], {constants.PRIORITY: 0} if (battle.opponent.active is None or battle.opponent.active.item != constants.UNKNOWN_ITEM or 'prankster' in [ normalize_name(a) for a in pokedex[battle.opponent.active.name] [constants.ABILITIES].values() ]): return moves = [ get_move_information(m) for m in msg_lines if m.startswith('|move|') ] if len(moves) != 2 or moves[0][0].startswith( battle.user.name ) or moves[0][1][constants.PRIORITY] != moves[1][1][constants.PRIORITY]: return battle_copy = deepcopy(battle) battle_copy.user.from_json(battle_copy.request_json) if battle.battle_type == constants.RANDOM_BATTLE: battle_copy.opponent.active.set_spread( 'serious', '85,85,85,85,85,85') # random battles have known spreads else: if battle.trick_room: battle_copy.opponent.active.set_spread( 'quiet', '0,0,0,0,0,0') # assume as slow as possible in trickroom else: battle_copy.opponent.active.set_spread( 'jolly', '0,0,0,0,0,252') # assume as fast as possible state = battle_copy.create_state() opponent_effective_speed = get_effective_speed(state, state.opponent) bot_effective_speed = get_effective_speed(state, state.self) if battle.trick_room: has_scarf = opponent_effective_speed > bot_effective_speed else: has_scarf = bot_effective_speed > opponent_effective_speed if has_scarf: logger.debug( "Opponent {} could not have gone first - setting it's item to choicescarf" .format(battle.opponent.active.name)) battle.opponent.active.item = 'choicescarf'
def start_volatile_status(battle, split_msg): if is_opponent(battle, split_msg): pkmn = battle.opponent.active else: pkmn = battle.user.active volatile_status = normalize_name(split_msg[3].split(":")[-1]) if volatile_status not in pkmn.volatile_statuses: logger.debug("Starting the volatile status {} on {}".format( volatile_status, pkmn.name)) pkmn.volatile_statuses.append(volatile_status)
def singleturn(battle, split_msg): if is_opponent(battle, split_msg): side = battle.opponent else: side = battle.user move_name = normalize_name(split_msg[3].split(':')[-1]) if move_name in constants.PROTECT_VOLATILE_STATUSES: # set to 2 because the `upkeep` function will decrement by 1 on every end-of-turn side.side_conditions[constants.PROTECT] = 2 logger.debug("{} used protect".format(side.active.name))
def sideend(battle, split_msg): """Remove a side effect such as stealth rock or sticky web""" condition = split_msg[3].split(':')[-1].strip() condition = normalize_name(condition) if is_opponent(battle, split_msg): logger.debug("Side condition {} ending for opponent".format(condition)) battle.opponent.side_conditions[condition] = 0 else: logger.debug("Side condition {} ending for bot".format(condition)) battle.user.side_conditions[condition] = 0
def fieldend(battle, split_msg): """Remove the battle's field condition""" field_name = normalize_name(split_msg[2].split(':')[-1].strip()) # trick room shows up as a `-fieldend` item but is separate from the other fields if field_name == constants.TRICK_ROOM: logger.debug("Removing trick room") battle.trick_room = False else: logger.debug("Setting the field to None") battle.field = None
def fieldstart(battle, split_msg): """Set the battle's field condition""" field_name = normalize_name(split_msg[2].split(':')[-1].strip()) # trick room shows up as a `-fieldstart` item but is separate from the other fields if field_name == constants.TRICK_ROOM: logger.debug("Setting trickroom") battle.trick_room = True else: logger.debug("Setting the field to {}".format(field_name)) battle.field = field_name