hsahovic / poke-env

A python interface for training Reinforcement Learning bots to battle on pokemon showdown
https://poke-env.readthedocs.io/
MIT License
290 stars 98 forks source link

Possible moves for mon #161

Open mancho2000 opened 3 years ago

mancho2000 commented 3 years ago

Hey,

I think adding the possible moves each mon can have would make agents much better, as they would know what to expect. Would it be possible to add them? As I understand, each mon has a pool of possible moves from which they get random moves. Also for OU and other similar tiers could get the moves from the monthly usage statistics, plus their use %.

hsahovic commented 3 years ago

Hey @mancho2000,

This is a good idea. poke-env currently contains a jsonified version of pokemon-showdown's learnset files, that could be used to do this. It is not used anywhere in the source code, apart from some scripts used for debugging.

Adding usage statistics on top of it would require a bit more effort, as it would come with a mechanism to keep the ground truth data updated with PS and cached locally.

Let me think about the problem a bit more - I will implement something along these lines in the next few weeks. In the meantime, don't hesitate to take a look at the script mentioned above if you want to implement a first version for your specific project. If you have some ideas regarding the API / related features, feel free to leave a comment!

Additionally, I saw that you closed #160 - did you find a solution? If not, I can take a deeper look at the problem if you want.

mancho2000 commented 3 years ago

I believe that random battles don't use the full learnset of each mon, but rather a small pool of moves from which it selects randomly. If we could use that small pool it would be very useful, but I think that using the whole learnset would be too inefficient to be of any use.

About the usage statistics, I saw Pmariglia's bot that uses them. Maybe it could be useful in some way: https://github.com/pmariglia/showdown/blob/master/data/parse_smogon_stats.py

And about the self play issue, I really don't know what caused it or how I solved it, but now its working fine :)

edit: He also has a json with random battle sets from which the move pools could be extracted, although I am not sure how accurate they are. https://github.com/pmariglia/showdown/blob/master/data/random_battle_sets.json

hsahovic commented 3 years ago

Interesting - I'll take a deeper look at it this week. This might indeed be an interesting feature :)

mancho2000 commented 3 years ago

I just noticed that sometimes a mon has only 1 possible ability, yet mon.ability returns None. I believe I fixed it like this:

    def ability(self) -> Optional[str]:
        """
        :return: The pokemon's ability. None if unknown.
        :rtype: str, optional
        """
        if self._ability:
            return self._ability
        else:
            if len(self._possible_abilities) == 1:
                for ability in self._possible_abilities.values():
                    return ability
            else:
                return self._ability

For sure there is a better way to do it, but that's all my programming skills allowed me to do haha :)

mancho2000 commented 3 years ago

hey!

any updates on this? :)

hsahovic commented 3 years ago

Hey @mancho2000,

It's been a few busy weeks - I'll try to dig into it over the weekend :)

mancho2000 commented 2 years ago

Hi Haris,

any estimate of when this will be done? :)

hsahovic commented 2 years ago

Hey @mancho2000,

Thanks for your patience - sorry it took me so long to get back to you. Implementing something like this into poke-env is part of the project, but doing so properly and robustly is hard. I don't know when it will be done.

That being said, here is an example script that should be easy to adapt to your specific use case. There are two main options:

  1. Enumerating all possible moves for a given mon
  2. Usage stats! Both are implemented below:
# -*- coding: utf-8 -*-
import asyncio

from poke_env.data import GEN8_POKEDEX
from poke_env.player.random_player import RandomPlayer
from poke_env.utils import to_id_str
from functools import lru_cache

import requests

MOVESETS = requests.get(
    "https://raw.githubusercontent.com/hsahovic/poke-env/master/src/poke_env/data/learnset.json"
).json()

# Replace file with the metagame you are interested in
USAGE_STATS = requests.get(
    "https://www.smogon.com/stats/2021-08/chaos/gen8anythinggoes-0.json"
).json()

@lru_cache(None)
def possible_moves(species):
    base_species = to_id_str(GEN8_POKEDEX[species]['baseSpecies'])

    if base_species not in MOVESETS:
        print("Missing species from movesets:", base_species)
        # You could return something else, like all moves, instead
        return []

    if "learnset" not in MOVESETS[base_species]:
        print("No learnset for", base_species)
        return []

    to_return = []

    for move, ways in MOVESETS[base_species]["learnset"].items():
        for way in ways:
            # 8 just means 8th gen - you can look up the different subtypes if you are
            # interested (eg. per level, eggs, tutors, etc)
            if way.startswith("8"):
                to_return.append(move)
                break

    print("All possible moves for", species, "are", to_return)

    return to_return

@lru_cache(None)
def moves_with_usage_stats(species):
    if species not in GEN8_POKEDEX:
        print("Missing species from dex:", species)
        return {}

    base_species = to_id_str(GEN8_POKEDEX[species]['baseSpecies'])
    name = GEN8_POKEDEX[base_species]['name']

    if name not in USAGE_STATS["data"]:
        # For some weird edge cases - this is slow, it might be better to init at startup
        id_name = to_id_str(name)
        for k in USAGE_STATS["data"]:
            if to_id_str(k) == id_name:
                name = k
                break
        else:
            print("Missing usage stats for", name)
            print([k for k in USAGE_STATS["data"] if k[0] == name[0]])
            return {}

    print("Moves with usage stats", species, "are", USAGE_STATS["data"][name]["Moves"])

    return USAGE_STATS["data"][name]["Moves"]

class CustomPlayer(RandomPlayer):
    def choose_move(self, battle):
        possible_moves(battle.opponent_active_pokemon.species)
        moves_with_usage_stats(battle.opponent_active_pokemon.species)
        return self.choose_random_move(battle)

async def main():
    random_player = RandomPlayer()
    custom_player = CustomPlayer()

    await random_player.battle_against(custom_player, 1)

if __name__ == "__main__":
    asyncio.get_event_loop().run_until_complete(main())

I resolved the issues I found when testing over a couple dozens battles - this is not thoroughly testes, but should work fine 99% of the time.