Réseau & file de commandes¶
La couche network/ est le transport du client : elle ouvre la socket,
déroule le handshake, découpe le flux TCP en lignes et fait respecter la règle
des 10 commandes en vol. Elle ne connaît rien à la sémantique du jeu — elle
manipule des chaînes brutes et délègue le parsing à la
couche protocole.
Quatre composants collaborent :
| Composant | Fichier | Rôle |
|---|---|---|
Connection |
ai/src/network/connection.py |
Socket TCP non bloquante + handshake initial |
Client |
ai/src/network/client.py |
Orchestration envoi/réception |
CommandQueue |
ai/src/network/command_queue.py |
File FIFO des commandes en vol (plafond de 10) |
InputBuffer / OutputBuffer |
ai/src/network/buffers.py |
Découpage en lignes du flux TCP |
Connection — socket & handshake¶
Connexion¶
connect() (ai/src/network/connection.py:32) crée une socket
AF_INET / SOCK_STREAM, se connecte en mode bloquant, puis bascule en
non bloquant :
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.sock.connect((self._host, self._port))
except ConnectionRefusedError as e:
...
raise Exception(f"Failed to connect to {self._host}:{self._port}") from e
self._sock.setblocking(False)
Connecter en bloquant puis passer non bloquant évite de gérer EINPROGRESS.
Une connexion refusée est convertie en Exception, ce qui remonte jusqu'à
run() et fait quitter avec le code 84.
Handshake¶
do_handshake() (ai/src/network/connection.py:65) suit la séquence du
protocole et renvoie (client_num, (width, height)) :
sequenceDiagram
participant S as Serveur
participant C as Connection
S->>C: WELCOME
C->>S: <team_name>\n
S->>C: <CLIENT-NUM>\n
S->>C: <X> <Y>\n
Note over C: renvoie (client_num, (width, height))
- Boucle de
select.select([sock], [], [], 0.1)jusqu'à recevoir un paquet ; si la première ligne décodée vautWELCOME, on sort de la boucle. - Envoi de
team_name + "\n". - Lecture d'un paquet, supposé contenir le
CLIENT-NUMet leX Y:splitlines()[0]doit être un entier (nombre de slots libres pour l'équipe),splitlines()[1]doit contenir une espace (width height).
Handshake fragile
Le handshake suppose que CLIENT-NUM et X Y arrivent dans le même
paquet TCP. S'ils sont fragmentés, splitlines()[1] lève un
IndexError. Voir Limitations connues.
Client — orchestration¶
Client (ai/src/network/client.py) détient une CommandQueue, un
InputBuffer, un OutputBuffer et la Connection. Il expose deux opérations
clés.
Envoi : send_command¶
send_command(command, callback) (ai/src/network/client.py:55) :
if not self.command_queue.can_send():
... # plafond de 10 atteint -> ignore
return
self.output_buffer.push(command + "\n") # ajoute le \n final
self.command_queue.enqueue(command, callback) # enregistre (command, callback)
...
self.output_buffer.flush_to(self.connection.sock) # envoi synchrone immédiat
Trois étapes : (1) vérifier le plafond ; (2) pousser command + "\n"
dans l'OutputBuffer (c'est ici, et non dans les
constructeurs de commandes, qu'on ajoute le \n) et
enregistrer le callback dans la CommandQueue ; (3) vider le tampon
immédiatement vers la socket. L'envoi est donc synchrone : pas de phase
d'écriture différée dans la boucle principale.
flush_output n'est pas utilisé
Client.flush_output (ai/src/network/client.py:126) est implémenté mais
jamais appelé depuis la boucle principale : l'écriture passe toujours par le
flush_to synchrone de send_command.
Réception : receive¶
receive() (ai/src/network/client.py:84) interroge la socket par
select.select([sock], [], [], 0.1) (poll de 100 ms). Un recv vide signale
une fermeture serveur (on annule connection). Sinon, les octets passent à
receive_data :
self.input_buffer.feed(data)
while True:
line = self.input_buffer.pop_line()
if line is None:
break
...
self.command_queue.on_response(line) # lignes BRUTES, non parsées ici
Le Client transmet des lignes brutes à CommandQueue.on_response ; le
parsing en messages typés est fait plus tard, dans les callbacks (cf.
Protocole).
Buffers — du flux aux lignes¶
Le protocole Zappy est orienté ligne (\n), mais TCP ne préserve pas les
frontières de message. Les buffers (ai/src/network/buffers.py) réconcilient
les deux.
InputBuffer.feedaccumule les octets ;InputBuffer.pop_lineextrait la première ligne complète (split(b"\n", 1), décodée UTF-8) ou renvoieNonesi aucune ligne n'est encore complète.OutputBuffer.pushencode et empile une chaîne ;flush_toenvoie autant d'octets que possible et conserve le reliquat en cas d'envoi partiel (self._buffer = self._buffer[sent:]), en absorbant lesBlockingIOError.
CommandQueue — les 10 commandes en vol¶
Le serveur Zappy accepte au plus 10 requêtes sans réponse par client (au-delà
elles sont ignorées) et répond toujours dans l'ordre de réception. La
CommandQueue (ai/src/network/command_queue.py) exploite cette garantie FIFO.
- Plafond :
can_send()renvoiepending() < MAX_PENDING_COMMANDS(MAX_PENDING_COMMANDS = 10,ai/src/config.py:29). C'est ce que teste la boucle principale avant de demander une commande à la FSM. - Appariement FIFO :
enqueue(command, callback)empile dans_inflight;on_responsedépile la tête de file et invoque le callback associé avec la ligne brute. - Désambiguïsation
Look/Inventory: après chaque réponse, la commande est mémorisée dans_last_command.peek_last_command()permet au parser de savoir si un[...]correspond à unLookou à unInventory. - Suivis non sollicités : si
_inflightest vide mais que_last_commandexiste,on_responseré-invoque le dernier callback (ai/src/network/command_queue.py:128-134). C'est ainsi qu'est traité leCurrent level: kqui suit une incantation réussie. - Mort : si la ligne reçue vaut exactement
"dead",on_responseappellesys.exit(0)(ai/src/network/command_queue.py:116-120).
Liste _callbacks morte
add_callback empile dans _callbacks, mais cette liste n'est jamais
relue dans on_response : seul le mécanisme _last_command ci-dessus est
effectif. Voir Limitations connues.
Séquence envoi puis réception¶
sequenceDiagram
participant FSM
participant Cl as Client
participant OB as OutputBuffer
participant CQ as CommandQueue
participant S as Serveur
participant IB as InputBuffer
Note over FSM,S: Envoi
FSM->>Cl: send_command("Look", cb)
Cl->>CQ: can_send() ? (pending < 10)
Cl->>OB: push("Look\n")
Cl->>CQ: enqueue("Look", cb)
Cl->>OB: flush_to(sock)
OB->>S: Look\n
Note over FSM,S: Réception
S-->>Cl: "[ player, food, ... ]"
Cl->>IB: feed(data)
IB-->>Cl: pop_line() -> "[ ... ]"
Cl->>CQ: on_response("[ ... ]")
CQ->>CQ: pop _inflight (FIFO) -> ("Look", cb)
CQ->>cb: cb("[ ... ]")
Pour aller plus loin¶
- Couche protocole — comment les lignes brutes deviennent des messages typés.
- Boucle de décision — l'enchaînement complet réception → décision → envoi.
- Limitations connues — fragilités du handshake, code mort.