Aller au contenu

Coordination d'équipe

Le paquet ai/src/team/ fournit une couche de communication chiffrée entre drones d'une même équipe, ainsi qu'un mécanisme de leurres pour induire les adversaires en erreur. Tout passe par la commande publique Broadcast du serveur.

Couche implémentée et testée, mais NON branchée

L'ensemble du paquet team/ est entièrement implémenté et couvert par des tests, mais aucun code du réseau ou de la stratégie ne l'appelle : ces modules ne sont référencés qu'entre eux. Concrètement, l'IA n'émet ni ne déchiffre aujourd'hui le moindre message d'équipe (le parseur de broadcast entrant est lui-même un stub, voir Couche protocole). Voir Limitations connues.

Pourquoi chiffrer ?

Broadcast est public : tout joueur à portée — y compris les équipes adverses — reçoit le message. Pour coordonner ses drones sans renseigner l'ennemi, l'équipe encode ses messages de façon illisible pour qui n'a pas la clé partagée. Les leurres, à l'inverse, sont volontairement en clair pour détourner l'attention adverse.

Ce n'est pas de la vraie cryptographie

Le chiffrement repose sur un simple XOR à clé répétée (style Vigenère sur les octets). L'objectif est uniquement de rendre le trafic illisible pour des IA adverses naïves, pas de résister à une cryptanalyse. C'est intentionnel et suffisant dans le contexte du jeu (ai/src/team/crypto.py:12-16).

crypto.py — primitives de chiffrement

ai/src/team/crypto.py transforme une chaîne en un jeton ASCII sur une seule ligne, transmissible via Broadcast (qui exige du texte imprimable sans \n) :

texte clair (str) --utf-8--> octets --XOR clé--> octets --base64--> jeton (str)
Fonction Rôle
xor_cipher(data, key) (ai/src/team/crypto.py:24) XOR octet à octet, clé répétée cycliquement. Involutive : xor_cipher(xor_cipher(d, k), k) == d, donc chiffre et déchiffre. Lève ValueError si la clé est vide.
encrypt(plaintext, key) (ai/src/team/crypto.py:45) texte → UTF-8 → XOR → base64 → jeton ASCII.
decrypt(token, key) (ai/src/team/crypto.py:60) inverse ; renvoie None si le jeton n'est pas du base64 valide ou si le résultat n'est pas de l'UTF-8 décodable.

Pourquoi decrypt renvoie None plutôt que de lever ?

Recevoir un message d'une équipe adverse est le cas normal : il échoue au déchiffrement. Renvoyer None permet de l'ignorer silencieusement au lieu de faire planter le drone (ai/src/team/crypto.py:75-83).

protocol.py — messages d'équipe typés

ai/src/team/protocol.py empile au-dessus de crypto une couche de messages typés. Avant chiffrement, un message a la forme ZPY|TYPE|payload :

  • la clé d'équipe TEAM_KEY = "ZPY" sert de préfixe magique (ai/src/team/protocol.py:17) ;
  • le séparateur est | (ai/src/team/protocol.py:19) ;
  • le type provient de l'énumération TeamMessageType (ai/src/team/protocol.py:22) : HERE_LVL, COME_LVL, STONES_NEED, LEVEL_UP_OK, ABORT, DECOY.

À quoi sert le préfixe ZPY ?

Après déchiffrement, du bruit adverse peut par hasard donner un texte imprimable. Le préfixe ZPY permet de distinguer un vrai message d'équipe d'un faux positif : sans ce préfixe, le message est rejeté.

Les deux fonctions sont tolérantes aux pannes (fail-soft) :

Fonction Rôle
encode_message(type, payload, key) (ai/src/team/protocol.py:33) construit "ZPY|TYPE|payload" puis le chiffre → jeton prêt pour Broadcast.
decode_message(token, key) (ai/src/team/protocol.py:52) déchiffre et valide : renvoie (type, payload) ou None si le déchiffrement échoue, si le préfixe est absent, si l'arité est mauvaise, ou si le type est inconnu.

decoy.py — leurres en clair

ai/src/team/decoy.py fabrique des messages-appâts : des broadcasts plausibles mais non chiffrés et sans préfixe d'équipe. Les alliés les rejettent systématiquement (ils échouent à decode_message), mais un adversaire naïf scrutant le trafic public peut gaspiller des ressources à y réagir (ai/src/team/decoy.py:1-7).

make_decoy_broadcast(seed=None) (ai/src/team/decoy.py:27) choisit un gabarit (_DECOY_TEMPLATES) et une ressource (STONES + ["food"]) de façon déterministe : via seed si fourni, sinon via un compteur interne qui fait varier la sortie d'un appel à l'autre. Exemples produits : need linemate at 3, regroup 5 for sibur

flowchart LR
    subgraph Allies["Alliés (clé ZPY)"]
        E["encode_message<br/>ZPY|TYPE|payload"] --> ENC["crypto.encrypt<br/>(XOR + base64)"]
        ENC --> BC["Broadcast (public)"]
        BC --> DEC["crypto.decrypt + decode_message"]
        DEC --> OK["(type, payload) valide"]
    end
    D["make_decoy_broadcast<br/>(texte en clair)"] --> BC
    BC --> ENNEMI["Adversaire naïf<br/>(peut réagir au leurre)"]
    DEC -.->|leurre rejeté| REJ["None (ignoré)"]

Documentation détaillée par module

Des notes par module existent dans le dépôt : ai/docs/team/CRYPTO.md, ai/docs/team/PROTOCOL.md et ai/docs/team/DECOY.md.

Pour aller plus loin

  • Couche protocole — le parseur de broadcast entrant (parse_broadcast) est un stub : tant qu'il n'est pas implémenté, les messages d'équipe reçus ne peuvent pas être décodés.
  • Stratégie (FSM) — où des messages d'équipe pourraient un jour déclencher des transitions (COME_LVL, STONES_NEED…).
  • Limitations connues — l'inventaire des morceaux non branchés.