Skip to content

Team coordination

The ai/src/team/ package provides a layer for encrypted communication between drones of the same team, plus a decoy mechanism to mislead opponents. Everything goes through the server's public Broadcast command.

Layer implemented and tested, but NOT wired up

The entire team/ package is fully implemented and covered by tests, but no network or strategy code calls it: these modules are only referenced by one another. In practice, the AI currently neither emits nor decrypts any team message (the inbound broadcast parser is itself a stub, see Protocol layer). See Known limitations.

Why encrypt?

Broadcast is public: every player in range — including opposing teams — receives the message. To coordinate its drones without informing the enemy, the team encodes its messages so they are unreadable to anyone without the shared key. Decoys, conversely, are deliberately in the clear to divert the opponent's attention.

This is not real cryptography

Encryption relies on a simple repeated-key XOR (Vigenère-style over bytes). The goal is only to make traffic unreadable to naive opposing AIs, not to resist cryptanalysis. This is intentional and sufficient in the game's context (ai/src/team/crypto.py:12-16).

crypto.py — encryption primitives

ai/src/team/crypto.py turns a string into a single-line ASCII token transmissible via Broadcast (which requires printable text with no \n):

plaintext (str) --utf-8--> bytes --XOR key--> bytes --base64--> token (str)
Function Role
xor_cipher(data, key) (ai/src/team/crypto.py:24) byte-by-byte XOR, key repeated cyclically. Involutive: xor_cipher(xor_cipher(d, k), k) == d, so it both encrypts and decrypts. Raises ValueError if the key is empty.
encrypt(plaintext, key) (ai/src/team/crypto.py:45) text → UTF-8 → XOR → base64 → ASCII token.
decrypt(token, key) (ai/src/team/crypto.py:60) inverse; returns None if the token is not valid base64 or if the result is not decodable UTF-8.

Why does decrypt return None instead of raising?

Receiving a message from an opposing team is the normal case: it fails decryption. Returning None lets it be silently ignored instead of crashing the drone (ai/src/team/crypto.py:75-83).

protocol.py — typed team messages

ai/src/team/protocol.py stacks a layer of typed messages on top of crypto. Before encryption, a message has the form ZPY|TYPE|payload:

  • the team key TEAM_KEY = "ZPY" serves as a magic prefix (ai/src/team/protocol.py:17);
  • the separator is | (ai/src/team/protocol.py:19);
  • the type comes from the TeamMessageType enum (ai/src/team/protocol.py:22): HERE_LVL, COME_LVL, STONES_NEED, LEVEL_UP_OK, ABORT, DECOY.

What is the ZPY prefix for?

After decryption, opposing noise may by chance decode into printable text. The ZPY prefix distinguishes a genuine team message from such a false positive: without the prefix, the message is rejected.

Both functions are fail-soft:

Function Role
encode_message(type, payload, key) (ai/src/team/protocol.py:33) builds "ZPY|TYPE|payload" then encrypts it → token ready for Broadcast.
decode_message(token, key) (ai/src/team/protocol.py:52) decrypts and validates: returns (type, payload) or None if decryption fails, the prefix is absent, the arity is wrong, or the type is unknown.

decoy.py — plaintext lures

ai/src/team/decoy.py builds lure messages: plausible broadcasts that are deliberately unencrypted and carry no team prefix. Allies always reject them (they fail decode_message), but a naive adversary scanning the public traffic may waste resources reacting to them (ai/src/team/decoy.py:1-7).

make_decoy_broadcast(seed=None) (ai/src/team/decoy.py:27) picks a template (_DECOY_TEMPLATES) and a resource (STONES + ["food"]) deterministically: via seed if provided, otherwise via an internal counter that varies the output from one call to the next. Example output: need linemate at 3, regroup 5 for sibur

flowchart LR
    subgraph Allies["Allies (key 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["valid (type, payload)"]
    end
    D["make_decoy_broadcast<br/>(plaintext)"] --> BC
    BC --> ENEMY["Naive adversary<br/>(may react to the lure)"]
    DEC -.->|lure rejected| REJ["None (ignored)"]

Per-module detailed docs

Per-module notes exist in the repository: ai/docs/team/CRYPTO.md, ai/docs/team/PROTOCOL.md and ai/docs/team/DECOY.md.

Going further

  • Protocol layer — the inbound broadcast parser (parse_broadcast) is a stub: until it is implemented, received team messages cannot be decoded.
  • Strategy (FSM) — where team messages could one day trigger transitions (COME_LVL, STONES_NEED…).
  • Known limitations — the inventory of unwired pieces.