Docs Faire écrire un Bot par l’IA

Itération autonome d'un agent

Laissez un agent de code jouer ses propres parties, lire les résultats, corriger le code et recommencer : il lui faut une commande qui tourne sans surveillance, un rapport de partie structuré et un objectif clair.

Dans Écrire un Bot avec un LLM, l’étape « jouer une partie → observer → le dire au modèle » vous revient. Un agent de code capable d’exécuter des commandes (Claude Code, Codex, le mode Agent de Cursor, etc.) peut aussi prendre cette étape en charge et boucler la boucle :

  corriger le code ──► jouer une partie (sans surveillance) ──► lire le rapport ──► trouver le point décisif ──┐
    ▲                                                                                                          │
    └──────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Pour que cette boucle converge vraiment, l’agent a besoin de trois choses.

1. Une commande qui tourne sans surveillance

python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
  • --minutes garantit que la partie se termine (en minutes réelles), l’agent ne restera pas bloqué dans une partie ;
  • --speed 200 fait gagner du temps en vitesse ×2 — mais dans le Bot, attendez selon l’horloge du jeu (g.clock()), pas avec un sleep en temps réel ;
  • --fair l’oblige dès le premier jour à respecter les règles de l’Arène : il ne voit que ce qui est dans son champ de vision ;
  • À la fin de l’exécution, le terminal affiche la raison de la fin, par exemple 我方没有单位了 (« plus aucune unité de notre côté ») ou 到时间了 (« temps écoulé ») ; ce que le Bot affiche lui-même avec print apparaît aussi dans le terminal.

Quand la fenêtre est réduite, la simulation du jeu est arrêtée. Faites lancer le jeu par l’agent en mode fenêtré par défaut, et évitez qu’il utilise le même numéro d’instance que celle que vous utilisez (--inst).

2. Un rapport de partie structuré

La sortie du terminal est faite pour les humains. Pour l’agent, il faut un JSON : ce qui s’est passé, ce qui a échoué, et pourquoi. Le SDK vous fournit déjà toute la matière première : les reçus portent des codes de motif, le flux d’événements contient les fins de production et les pertes. Il suffit de les rassembler :

import collections, json, time
from openwar3 import Bot

class Recorder(Bot):
    """Ajoute un rapport de partie au Bot. Héritez-en, puis appelez super() dans vos propres on_start / on_event."""

    def on_start(self, g):
        self.rejects = collections.Counter()   # "train hfoo: rejected(人口不够)" (= nourriture insuffisante) -> nombre d'occurrences
        self.timeline = []                     # [secondes de jeu, catégorie, code à quatre caractères] : entraînement / recherche / construction / amélioration terminés
        self.lost = collections.Counter()      # ce que nous avons perdu
        self.killed = collections.Counter()    # ce que nous avons tué

    def check(self, r, what):
        """Enveloppe une commande pour noter la raison du rejet : self.check(g.train(b, "hfoo"), "train hfoo")"""
        if r is not None and not r:
            self.rejects[f"{what}: {r.reason}"] += 1
        return r

    def on_event(self, g, ev):
        me = g.me()
        if ev.kind == "production.done" and ev.owner == me:
            self.timeline.append([round(ev.clock), ev.done_kind, ev.done_code])
        elif ev.kind == "unit.died":
            (self.lost if ev.owner == me else self.killed)[ev.type] += 1

    def on_end(self, g, reason):
        report = {"reason": reason, "timeline": self.timeline, "lost": self.lost,
                  "killed": self.killed, "rejects": self.rejects.most_common(10)}
        try:                                   # le jeu est peut-être déjà fermé ; si la lecture échoue, tant pis
            report |= {"clock": g.clock(), "resources": g.resources(),
                       "army": len(g.my_army()), "workers": len(g.my_workers())}
        except Exception:
            pass
        with open(f"run_{int(time.time())}.json", "w", encoding="utf-8") as f:
            json.dump(report, f, ensure_ascii=False, indent=1)

Les questions auxquelles ce rapport répond :

SignalSourceCe qu’il révèle
Raisons de rejet les plus fréquentesreason / verdict du reçuNourriture constamment bloquée (3), ordres donnés sans assez d’argent (8 / 9), attaques sur des cibles dans le brouillard (1001), entraînement d’un héros déjà mort au lieu de le ressusciter (221)
Chronologie de productionÉvénements production.done (avec le nombre de secondes de jeu nécessaires)À quelle seconde sort le premier héros, à quelle seconde vous passez de tier, si la caserne produit en continu ; comparable aux ouvertures des joueurs pros
Pertes des deux campsÉvénements unit.diedSi vous offrez des unités en continu, combien de fois le héros est mort, si le creeping a été rentable
Raison de la finon_end(g, reason)我方没有单位了 (plus aucune unité) = défaite ; 到时间了 (temps écoulé) = pas encore de vainqueur
Armée et ressources finalesUn instantané lu dans on_endDe l’argent qui dort = la production ne suit pas ; trop peu d’ouvriers = l’économie n’a pas décollé

La détermination programmatique du vainqueur fait partie des expériences fondatrices de l’Arène et figure encore sur la feuille de route. Pour l’instant, vous pouvez considérer « plus aucune unité de notre côté » comme une défaite, et approcher la victoire par « tous les bâtiments ennemis visibles sont détruits ».

3. Un objectif clair et quelques contraintes

Confiez le texte suivant à l’agent, en l’adaptant à votre objectif :

Objectif : faire en sorte que brains/my_bot.py batte de façon fiable l'ordinateur en difficulté « facile » sur Echo Isles (Humains contre race aléatoire).

À chaque itération :
1. Exécute python tools/play.py --bot brains/my_bot.py --speed 200 --minutes 10 --fair
2. Lis la sortie du terminal et le run_*.json le plus récent : raison de la fin, chronologie de production, raisons de rejet les plus fréquentes, pertes des deux camps
3. Trouve « un seul » problème, celui qui pèse le plus sur le résultat, et ne corrige que celui-là ; note en commentaire dans le code la raison du changement et les données sur lesquelles il s'appuie
4. Reviens à l'étape 1. Après 3 parties d'affilée sans progrès, arrête-toi et communique-moi le rapport et ton analyse

Contraintes :
- Utilise uniquement les méthodes de docs/api.json, n'invente pas d'interface
- Ne redonne pas le même ordre à la même unité à chaque tick ; ne donne des ordres qu'aux unités inactives
- Garde --fair (uniquement les ennemis visibles dans le champ de vision)
- Avant de modifier le code, exécute python tools/run_tests.py pour vérifier que les exemples ne sont pas cassés

Quelques habitudes pour faire converger la boucle plus vite

  • Une seule modification à la fois. Si vous changez trois choses en même temps, vous ne saurez ni laquelle a fait gagner, ni laquelle a tout cassé.
  • Jouez assez de parties pour comparer. Une même situation comporte beaucoup d’aléa ; deux parties ne révèlent que les très grands écarts. Pour juger d’un progrès, regardez au moins la tendance sur plusieurs parties.
  • Corrigez les « rejets » avant d’ajuster la stratégie. La raison de rejet la plus fréquente dans les reçus est souvent le plus gros bug du Bot.
  • Écrivez votre raisonnement dans les commentaires. L’agent de l’itération suivante (ou de la conversation suivante) comprendra pourquoi le code est ainsi et ne défera pas ce qui a été corrigé.
  • Des tests hors ligne comme filet de sécurité. Écrivez pour la logique critique des tests unitaires qui n’ont pas besoin de lancer le jeu (les tests des Bots d’exemple sont dans brains/examples/tests/), et faites-les exécuter par l’agent après chaque modification.