#
Construire un agent de combat Pokémon

Maintenant que vous avez exploré le potentiel et les limitations de l’IA agentique dans les jeux vidéos, il est temps de passer à la pratique. Dans cette section, vous allez construire votre propre agent pour combattre dans un combat au tour par tour dans le style de Pokémon, en utilisant tout ce que vous avez appris à travers le cours.

Nous décomposerons le système en quatre blocs de construction clés :

- Poke-env : Une bibliothèque Python conçue pour entraîner des bots Pokémon basés sur des règles ou par apprentissage par renforcement.

- Pokémon Showdown : Un simulateur de combat en ligne où votre agent combattra.

- LLMAgentBase : Une classe Python personnalisée que nous avons construite pour connecter votre LLM avec l’environnement de combat Poke-env.

- TemplateAgent : Un template de démarrage que vous compléterez pour créer votre propre agent de combat personnalisé.

Explorons chacun de ces composants plus en détail.

##
🧠 Poke-env

Poke-env est une interface Python originellement construite pour entraîner des bots d’apprentissage par renforcement par Haris Sahovic, mais nous l’avons réutilisée pour l’IA agentique.
Elle permet à votre agent d’interagir avec Pokémon Showdown à travers une API simple.

Elle fournit une classe `Player` de laquelle votre agent héritera, couvrant tout ce qui est nécessaire pour communiquer avec l’interface graphique.

Documentation : poke-env.readthedocs.io
Dépôt : github.com/hsahovic/poke-env

##
⚔️ Pokémon Showdown

Pokémon Showdown est un simulateur de combat open-source où votre agent jouera des combats Pokémon en direct.
Il fournit une interface complète pour simuler et afficher des combats en temps réel. Dans notre défi, le bot agira exactement comme un joueur humain, choisissant des mouvements tour par tour.

Nous avons déployé un serveur que tous les participants utiliseront pour combattre. Voyons qui construit le meilleur agent de combat !

Dépôt : github.com/smogon/Pokemon-Showdown
Site web : pokemonshowdown.com

##
🔌 LLMAgentBase

`LLMAgentBase` est une classe Python qui étend la classe `Player` de Poke-env.
Elle sert de pont entre votre LLM et le simulateur de combat Pokémon, gérant le formatage d’entrée/sortie et maintenant le contexte de combat.

Cet agent de base fournit un ensemble d’outils (définis dans `STANDARD_TOOL_SCHEMA`) pour interagir avec l’environnement, incluant :

- `choose_move` : pour sélectionner une attaque pendant le combat
- `choose_switch` : pour changer de Pokémon

Le LLM devrait utiliser ces outils pour prendre des décisions pendant un match.

###
🧠 Logique centrale

- `choose_move(battle: Battle)` : La méthode principale invoquée à chaque tour. Elle prend un objet `Battle` et retourne une chaîne d’action basée sur la sortie du LLM.

###
🔧 Méthodes internes clés

- `_format_battle_state(battle)` : Convertit l’état actuel du combat en chaîne, la rendant adaptée pour l’envoi au LLM.

- `_find_move_by_name(battle, move_name)` : Trouve un mouvement par nom, utilisé dans les réponses LLM qui appellent `choose_move`.

- `_find_pokemon_by_name(battle, pokemon_name)` : Localise un Pokémon spécifique vers lequel changer, basé sur la commande de changement du LLM.

- `_get_llm_decision(battle_state)` : Cette méthode est abstraite dans la classe de base. Vous devrez l’implémenter dans votre propre agent (voir prochaine section), où vous définissez comment interroger le LLM et analyser sa réponse.

Voici un extrait montrant comment cette prise de décision fonctionne :

    STANDARD_TOOL_SCHEMA = {
        "choose_move": {
            ...
        },
        "choose_switch": {
            ...
        },
    }

    class LLMAgentBase(Player):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.standard_tools = STANDARD_TOOL_SCHEMA
            self.battle_history = []

        def _format_battle_state(self, battle: Battle) -> str:
            active_pkmn = battle.active_pokemon
            active_pkmn_info = f"Your active Pokemon: {active_pkmn.species} " \
                               f"(Type: {'/'.join(map(str, active_pkmn.types))}) " \
                               f"HP: {active_pkmn.current_hp_fraction * 100:.1f}% " \
                               f"Status: {active_pkmn.status.name if active_pkmn.status else 'None'} " \
                               f"Boosts: {active_pkmn.boosts}"

            opponent_pkmn = battle.opponent_active_pokemon
            opp_info_str = "Unknown"
            if opponent_pkmn:
                opp_info_str = f"{opponent_pkmn.species} " \
                               f"(Type: {'/'.join(map(str, opponent_pkmn.types))}) " \
                               f"HP: {opponent_pkmn.current_hp_fraction * 100:.1f}% " \
                               f"Status: {opponent_pkmn.status.name if opponent_pkmn.status else 'None'} " \
                               f"Boosts: {opponent_pkmn.boosts}"
            opponent_pkmn_info = f"Opponent's active Pokemon: {opp_info_str}"

            available_moves_info = "Available moves:\n"
            if battle.available_moves:
                available_moves_info += "\n".join(
                    [f"- {move.id} (Type: {move.type}, BP: {move.base_power}, Acc: {move.accuracy}, PP: {move.current_pp}/{move.max_pp}, Cat: {move.category.name})"
                     for move in battle.available_moves]
                )
            else:
                 available_moves_info += "- None (Must switch or Struggle)"

            available_switches_info = "Available switches:\n"
            if battle.available_switches:
                  available_switches_info += "\n".join(
                      [f"- {pkmn.species} (HP: {pkmn.current_hp_fraction * 100:.1f}%, Status: {pkmn.status.name if pkmn.status else 'None'})"
                       for pkmn in battle.available_switches]
                  )
            else:
                available_switches_info += "- None"

            state_str = f"{active_pkmn_info}\n" \
                        f"{opponent_pkmn_info}\n\n" \
                        f"{available_moves_info}\n\n" \
                        f"{available_switches_info}\n\n" \
                        f"Weather: {battle.weather}\n" \
                        f"Terrains: {battle.fields}\n" \
                        f"Your Side Conditions: {battle.side_conditions}\n" \
                        f"Opponent Side Conditions: {battle.opponent_side_conditions}"
            return state_str.strip()

        def _find_move_by_name(self, battle: Battle, move_name: str) -> Optional[Move]:
            normalized_name = normalize_name(move_name)
            # Prioriser la correspondance exacte d'ID
            for move in battle.available_moves:
                if move.id == normalized_name:
                    return move
            # Solution de secours : Vérifier le nom d'affichage (moins fiable)
            for move in battle.available_moves:
                if move.name.lower() == move_name.lower():
                    print(f"Warning: Matched move by display name '{move.name}' instead of ID '{move.id}'. Input was '{move_name}'.")
                    return move
            return None

        def _find_pokemon_by_name(self, battle: Battle, pokemon_name: str) -> Optional[Pokemon]:
            normalized_name = normalize_name(pokemon_name)
            for pkmn in battle.available_switches:
                # Normaliser le nom d'espèce pour la comparaison
                if normalize_name(pkmn.species) == normalized_name:
                    return pkmn
            return None

        async def choose_move(self, battle: Battle) -> str:
            battle_state_str = self._format_battle_state(battle)
            decision_result = await self._get_llm_decision(battle_state_str)
            print(decision_result)
            decision = decision_result.get("decision")
            error_message = decision_result.get("error")
            action_taken = False
            fallback_reason = ""

            if decision:
                function_name = decision.get("name")
                args = decision.get("arguments", {})
                if function_name == "choose_move":
                    move_name = args.get("move_name")
                    if move_name:
                        chosen_move = self._find_move_by_name(battle, move_name)
                        if chosen_move and chosen_move in battle.available_moves:
                            action_taken = True
                            chat_msg = f"AI Decision: Using move '{chosen_move.id}'."
                            print(chat_msg)
                            return self.create_order(chosen_move)
                        else:
                            fallback_reason = f"LLM chose unavailable/invalid move '{move_name}'."
                    else:
                         fallback_reason = "LLM 'choose_move' called without 'move_name'."
                elif function_name == "choose_switch":
                    pokemon_name = args.get("pokemon_name")
                    if pokemon_name:
                        chosen_switch = self._find_pokemon_by_name(battle, pokemon_name)
                        if chosen_switch and chosen_switch in battle.available_switches:
                            action_taken = True
                            chat_msg = f"AI Decision: Switching to '{chosen_switch.species}'."
                            print(chat_msg)
                            return self.create_order(chosen_switch)
                        else:
                            fallback_reason = f"LLM chose unavailable/invalid switch '{pokemon_name}'."
                    else:
                        fallback_reason = "LLM 'choose_switch' called without 'pokemon_name'."
                else:
                    fallback_reason = f"LLM called unknown function '{function_name}'."

            if not action_taken:
                if not fallback_reason:
                     if error_message:
                         fallback_reason = f"API Error: {error_message}"
                     elif decision is None:
                          fallback_reason = "LLM did not provide a valid function call."
                     else:
                          fallback_reason = "Unknown error processing LLM decision."

                print(f"Warning: {fallback_reason} Choosing random action.")

                if battle.available_moves or battle.available_switches:
                     return self.choose_random_move(battle)
                else:
                     print("AI Fallback: No moves or switches available. Using Struggle/Default.")
                     return self.choose_default_move(battle)

        async def _get_llm_decision(self, battle_state: str) -> Dict[str, Any]:
            raise NotImplementedError("Subclasses must implement _get_llm_decision")

Code source complet : agents.py

##
🧪 TemplateAgent

Maintenant vient la partie amusante ! Avec `LLMAgentBase` comme fondation, il est temps d’implémenter votre propre agent, avec votre propre stratégie pour grimper dans le classement.

Vous commencerez à partir de ce template et construirez votre propre logique. Nous avons aussi fourni trois exemples complets utilisant les modèles OpenAI, Mistral et Gemini pour vous guider.

Voici une version simplifiée du template :

    class TemplateAgent(LLMAgentBase):
        """Utilise l'API Template AI pour prendre des décisions."""
        def __init__(self, api_key: str = None, model: str = "model-name", *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.model = model
            self.template_client = TemplateModelProvider(api_key=...)
            self.template_tools = list(self.standard_tools.values())

        async def _get_llm_decision(self, battle_state: str) -> Dict[str, Any]:
            """Envoie l'état au LLM et reçoit en retour la décision relative à l'appel de fonction."""
            system_prompt = (
                "You are a ..."
            )
            user_prompt = f"..."

            try:
                response = await self.template_client.chat.completions.create(
                    model=self.model,
                    messages=[
                        {"role": "system", "content": system_prompt},
                        {"role": "user", "content": user_prompt},
                    ],
                )
                message = response.choices[0].message

                return {"decision": {"name": function_name, "arguments": arguments}}

            except Exception as e:
                print(f"Unexpected error during call: {e}")
                return {"error": f"Unexpected error: {e}"}

Ce code ne fonctionnera pas directement, c’est un plan pour votre cas personnalisé.

Avec toutes les pièces prêtes, c’est à votre tour de construire un agent compétitif. Dans la prochaine section, nous montrerons comment déployer votre agent sur notre serveur et combattre d’autres en temps réel.

Que le combat commence ! 🔥

Update on GitHub