def evaluate_matchup(attacker: Pokemon, boss: Pokemon, teammates: Iterable[Pokemon] = [], num_lives: int = 1) -> float: """Return a matchup score between an attacker and defender, with the attacker using optimal moves and the defender using average moves. """ # Transform Ditto (Dynamax Adventure Ditto has Imposter). if attacker.name_id == 'ditto': attacker = transform_ditto(attacker, boss) elif boss.name_id == 'ditto': boss = transform_ditto(boss, attacker) # Calculate scores for base and Dynamaxed versions of the attacker. base_version = copy.copy(attacker) base_version.dynamax = False dmax_version = copy.copy(attacker) dmax_version.dynamax = True base_version_score = select_best_move(base_version, boss, Field(), teammates)[2] dmax_version_score = select_best_move(dmax_version, boss, Field(), teammates)[2] score = max(base_version_score, (base_version_score + dmax_version_score) / 2) # Adjust the score depending on the Pokemon's HP. If there are multiple # lives left, fainting is less important. assert 1 <= num_lives <= 4, 'num_lives should be between 1 and 4.' HP_correction = ((5 - num_lives) * attacker.HP + num_lives - 1) / 4 return score * HP_correction
def weather_damage_multiplier(attacker: Pokemon, move: Move, defender: Pokemon, field: Field) -> float: modifier = 1 if field.is_weather_sunlight(): if move.name_id == 'solar-beam': modifier *= 2 elif move.name_id == 'thunder' or move.name_id == 'hurricane': modifier *= (0.5 / 0.7) elif move.name_id == 'weather-ball': move.type_id = 'fire' modifier *= 2 if move.type_id == 'fire': modifier *= 1.5 elif move.type_id == 'water': modifier *= 0.5 elif field.is_weather_rain(): if move.name_id == 'solar-beam': modifier *= 0.5 elif move.name_id == 'thunder' or move.name_id == 'hurricane': modifier *= (1.0 / 0.7) elif move.name_id == 'weather-ball': move.type_id = 'water' modifier *= 2 if move.type_id == 'water': modifier *= 1.5 elif move.type_id == 'fire': modifier *= 0.5 elif field.is_weather_hail(): if move.name_id == 'solar-beam': modifier *= 0.5 elif move.name_id == 'blizzard': modifier *= (1.0 / 0.7) elif move.name_id == 'weather-ball': move.type_id = 'ice' modifier *= 2 elif field.is_weather_sandstorm(): if move.name_id == 'solar-beam': modifier *= 0.5 elif move.name_id == 'weather-ball': move.type_id = 'rock' modifier *= 2 else: if move.name_id == 'weather-ball': move.type_id = 'normal' return modifier
def terrain_damage_multiplier(attacker: Pokemon, move: Move, defender: Pokemon, field: Field) -> float: modifier = 1.0 if 'flying' not in attacker.type_ids or ('levitate' != attacker.ability_name_id): if field.is_terrain_electric(): if move.name_id == 'terrain-pulse': move.type_id = 'electric' modifier *= 1.5 if move.type_id == 'electric': modifier *= 1.3 elif field.is_terrain_grassy(): if move.name_id == 'terrain-pulse': move.type_id = 'grass' modifier *= 1.5 if move.type_id == 'grass': modifier *= 1.3 elif field.is_terrain_psychic(): if move.name_id == 'terrain-pulse': move.type_id = 'psychic' modifier *= 1.5 elif move.name_id == 'expanding-force': modifier *= 1.5 if move.type_id == 'psychic': modifier *= 1.3 elif field.is_terrain_misty(): if move.name_id == 'terrain-pulse': move.type_id = 'fairy' modifier *= 1.5 if move.type_id == 'dragon': modifier *= 0.5 if 'flying' not in defender.type_ids or ('levitate' != defender.ability_name_id): if field.is_terrain_electric(): if move.name_id == 'rising-voltage': modifier *= 2 return modifier
def reset_stage(self) -> None: """Reset after a battle.""" self.move_index = 0 self.dmax_timer = -1 self.opponent = None self.dynamax_available = False self.field = Field() if self.pokemon is not None: if self.pokemon.name_id == 'ditto': self.pokemon = self.rental_pokemon['ditto'] self.pokemon.dynamax = False
def test_weather(rental_pokemon, boss_pokemon): print('Weather tests') field_clear = Field() field_clear.set_weather_clear() field_rain = Field() field_rain.set_weather_rain() field_sandstorm = Field() field_sandstorm.set_weather_sandstorm() field_sunlingt = Field() field_sunlingt.set_weather_sunlight() field_hail = Field() field_hail.set_weather_hail() print('Solar beam (clear / rain / sandstorm / sun / hail)') for field in [ field_clear, field_rain, field_sandstorm, field_sunlingt, field_hail ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['exeggutor'], 1, boss_pokemon['guzzlord'], field) print(f'Damage is {dmg}') print('Water attack (clear / rain / sandstorm / sun / hail)') for field in [ field_clear, field_rain, field_sandstorm, field_sunlingt, field_hail ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['vaporeon'], 0, boss_pokemon['guzzlord'], field) print(f'Damage is {dmg}') print('Fire attack (clear / rain / sandstorm / sun / hail)') for field in [ field_clear, field_rain, field_sandstorm, field_sunlingt, field_hail ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['flareon'], 0, boss_pokemon['guzzlord'], field) print(f'Damage is {dmg}') print( 'Weather ball attack vs ground type (clear / rain / sandstorm / sun / hail)' ) for field in [ field_clear, field_rain, field_sandstorm, field_sunlingt, field_hail ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['roselia'], 2, rental_pokemon['marowak'], field) print(f'Damage is {dmg}') print( 'Weather ball attack vs flying type (clear / rain / sandstorm / sun / hail)' ) for field in [ field_clear, field_rain, field_sandstorm, field_sunlingt, field_hail ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['roselia'], 2, rental_pokemon['unfezant'], field) print(f'Damage is {dmg}') print( 'Weather ball attack vs grass type (clear / rain / sandstorm / sun / hail)' ) for field in [ field_clear, field_rain, field_sandstorm, field_sunlingt, field_hail ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['roselia'], 2, rental_pokemon['ivysaur'], field) print(f'Damage is {dmg}') print( 'Special attack vs rock type (clear / rain / sandstorm / sun / hail)') for field in [ field_clear, field_rain, field_sandstorm, field_sunlingt, field_hail ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['roselia'], 0, rental_pokemon['sudowoodo'], field) print(f'Damage is {dmg}')
def main(): with open(os.path.join(base_dir, 'data', 'boss_pokemon.json'), 'r', encoding='utf8') as file: boss_pokemon = jsonpickle.decode(file.read()) with open(os.path.join(base_dir, 'data', 'rental_pokemon.json'), 'r', encoding='utf8') as file: rental_pokemon = jsonpickle.decode(file.read()) with open(os.path.join(base_dir, 'data', 'boss_matchup_LUT.json'), 'r', encoding='utf8') as file: boss_matchups = jsonpickle.decode(file.read()) with open(os.path.join(base_dir, 'data', 'rental_matchup_LUT.json'), 'r', encoding='utf8') as file: rental_matchups = jsonpickle.decode(file.read()) with open(os.path.join(base_dir, 'data', 'rental_pokemon_scores.json'), 'r', encoding='utf8') as file: rental_scores = jsonpickle.decode(file.read()) # Test retrieval of a rental Pokemon rental_pokemon['stunfisk-galar'].print_verbose() print('________________________________________') # Test retrieval of a boss Pokemon boss_pokemon['mewtwo'].print_verbose() print('________________________________________') # Test retrieval of rental Pokemon matchups print('Matchup for Chansey against Golurk (poor): ' f'{rental_matchups["chansey"]["golurk"]}') print('Matchup for Carkol against Butterfree (good): ' f'{rental_matchups["carkol"]["butterfree"]}') print('________________________________________') # Test retrieval of boss Pokemon matchups print('Matchup for Jynx against Heatran (poor): ' f'{boss_matchups["jynx"]["heatran"]}') print('Matchup for Golurk against Raikou (good): ' f'{boss_matchups["golurk"]["raikou"]}') print('________________________________________') # Test retrieval of rental Pokemon scores print(f'Score for Jigglypuff (poor): {rental_scores["jigglypuff"]}') print(f'Score for Doublade (good): {rental_scores["doublade"]}') print('________________________________________') # Test move selection print('Wide Guard utility:') matchup_scoring.print_matchup_summary(rental_pokemon['pelipper'], boss_pokemon['groudon'], Field(), rental_pokemon.values()) salazzle = rental_pokemon['salazzle'] print('Regular matchup:') matchup_scoring.print_matchup_summary(salazzle, boss_pokemon['kartana'], Field(), rental_pokemon.values()) print('Max move scores:') salazzle.dynamax = True matchup_scoring.print_matchup_summary(salazzle, boss_pokemon['kartana'], Field(), rental_pokemon.values()) print('Sap Sipper:') matchup_scoring.print_matchup_summary(rental_pokemon['tsareena'], rental_pokemon['azumarill'], Field(), rental_pokemon.values()) print('________________________________________') # Ensure all rental Pokemon have sprites with open(os.path.join(base_dir, 'data', 'pokemon_sprites.pickle'), 'rb') as file: pokemon_sprites = pickle.load(file) pokemon_sprites_dict = dict(pokemon_sprites) for name_id in rental_pokemon: if pokemon_sprites_dict.get(name_id) is None: raise KeyError(f'ERROR: no image found for: {name_id}') print('Successfully tested Pokemon sprite importing without errors.') print('________________________________________') test_field(rental_pokemon, boss_pokemon)
def test_terrain(rental_pokemon, boss_pokemon): print('Terrain tests') field_clear = Field() field_clear.set_terrain_clear() field_electric = Field() field_electric.set_terrain_electric() field_psychic = Field() field_psychic.set_terrain_psychic() field_grassy = Field() field_grassy.set_terrain_grassy() field_misty = Field() field_misty.set_terrain_misty() print('electric move (clear / electric / psychic / grassy / misty)') for field in [ field_clear, field_electric, field_psychic, field_grassy, field_misty ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['electabuzz'], 1, boss_pokemon['guzzlord'], field) print(f'Damage is {dmg}') print('rising voltage (clear / electric / psychic / grassy / misty)') for field in [ field_clear, field_electric, field_psychic, field_grassy, field_misty ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['electabuzz'], 0, boss_pokemon['guzzlord'], field) print(f'Damage is {dmg}') print('psychic move (clear / electric / psychic / grassy / misty)') for field in [ field_clear, field_electric, field_psychic, field_grassy, field_misty ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['slowbro'], 3, rental_pokemon['electabuzz'], field) print(f'Damage is {dmg}') print('expanding force (clear / electric / psychic / grassy / misty)') for field in [ field_clear, field_electric, field_psychic, field_grassy, field_misty ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['slowbro'], 0, rental_pokemon['electabuzz'], field) print(f'Damage is {dmg}') print('dragon move (clear / electric / psychic / grassy / misty)') for field in [ field_clear, field_electric, field_psychic, field_grassy, field_misty ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['charmeleon'], 1, rental_pokemon['electabuzz'], field) print(f'Damage is {dmg}') print('grass move (clear / electric / psychic / grassy / misty)') for field in [ field_clear, field_electric, field_psychic, field_grassy, field_misty ]: dmg = matchup_scoring.calculate_damage(rental_pokemon['tangela'], 2, rental_pokemon['electabuzz'], field) print(f'Damage is {dmg}')
def calculate_damage(attacker: Pokemon, move_index: int, defender: Pokemon, field: Field, multiple_targets: bool = False) -> float: """Return the damage (default %) of a move used by the attacker against the defender. """ move = (attacker.max_moves[move_index] if attacker.dynamax else attacker.moves[move_index]) modifier = 0.925 # Random between 0.85 and 1 modifier *= move.accuracy if multiple_targets and move.is_spread: modifier *= 0.75 # Ignore crits # It needs to be before STAB modifier *= weather_damage_multiplier(attacker, move, defender, field) modifier *= terrain_damage_multiplier(attacker, move, defender, field) # It needs to be before STAB if move.type_id == 'normal': if attacker.ability_name_id == 'refrigerate': move.type_id = 'ice' modifier *= 1.2 elif attacker.ability_name_id == 'aerilate': move.type_id = 'flying' modifier *= 1.2 elif attacker.ability_name_id == 'galvanize': move.type_id = 'electric' modifier *= 1.2 elif attacker.ability_name_id == 'pixilate': move.type_id = 'fairy' modifier *= 1.2 else: if attacker.ability_name_id == 'normalize': move.type_id = 'normal' modifier *= 1.2 if move.type_id in attacker.type_ids: # Apply STAB # Note that Adaptability is handled elsewhere. modifier *= 1.5 # Apply type effectiveness if move.name_id != 'thousand-arrows' or 'flying' not in defender.type_ids: modifier *= type_damage_multiplier(move.type_id, defender.type_ids) # Apply status effects if move.category == 'physical' and attacker.status == 'burn': modifier *= 0.5 # Apply modifiers from abilities modifier *= ability_damage_multiplier(attacker, move_index, defender) # Apply boosts from auras if move.type_id == 'fairy' and (attacker.ability_name_id == 'fairy-aura' or defender.ability_name_id == 'fairy-aura'): modifier *= 1.33 if move.type_id == 'dark' and (attacker.ability_name_id == 'dark-aura' or defender.ability_name_id == 'dark-aura'): modifier *= 1.33 # Apply attacker and defender stats if move.category == 'physical': if move.name_id == 'body-press': numerator = attacker.stats[2] elif move.name_id == 'foul-play': numerator = defender.stats[1] else: numerator = attacker.stats[1] denominator = defender.stats[2] else: numerator = attacker.stats[3] if move.name_id not in ('psystrike', 'psyshock'): denominator = defender.stats[4] if field.is_weather_sandstorm() and 'rock' in defender.type_ids: denominator *= 1.5 else: denominator = defender.stats[2] return (((2 / 5 * attacker.level + 2) * move.power * numerator / denominator / 50 + 2) * modifier / defender.stats[0])