Boucle de décision¶
Cette page assemble toutes les couches précédentes en un flux d'exécution
bout-en-bout : du paquet TCP brut reçu jusqu'à l'action renvoyée au serveur.
C'est la boucle principale de main.run() (ai/src/main.py:127) qui orchestre
le tout.
Vue d'ensemble¶
La boucle est mono-thread et alterne deux phases à chaque itération :
- Réception — vider la socket, parser chaque ligne, mettre à jour l'état.
- Décision — tant que la file de commandes n'est pas pleine, demander à la FSM la prochaine action et l'émettre.
while 1:
client.receive() # phase réception
while client.command_queue.can_send(): # phase décision
if not fsm.decide_next_command():
break
if drone.is_dead() and config.INIT:
break
Polling par select-with-timeout, pas selectors
Les docstrings de main.py et le README évoquent une boucle selectors sans
attente active. Le code réel utilise un while 1 qui interroge la socket
par select.select([sock], [], [], 0.1) (ai/src/network/client.py:89) :
un polling à 100 ms, pas le module selectors. Voir
Limitations connues.
Phase de réception, étape par étape¶
Client.receive() (ai/src/network/client.py:84) lit la socket non bloquante
tant que select la signale prête, par paquets de 4096 octets. Un recv vide
signifie que le serveur a fermé la connexion (connection = None).
Chaque paquet passe dans receive_data (ai/src/network/client.py:100) :
InputBuffer.feed(data)accumule les octets bruts (ai/src/network/buffers.py:24) ;InputBuffer.pop_line()extrait les lignes complètes (séparées par\n) une à une (ai/src/network/buffers.py:38) ;- chaque ligne est transmise telle quelle (non parsée) à
CommandQueue.on_response(line)(ai/src/network/client.py:124).
Pourquoi un buffer de lignes ?
TCP est un flux d'octets sans frontières de message : un recv peut livrer
une demi-ligne, ou trois lignes d'un coup. Le buffer rétablit le découpage en
lignes qu'exige le protocole orienté texte de Zappy.
Appariement FIFO et déclenchement du callback¶
CommandQueue.on_response (ai/src/network/command_queue.py:100) associe la
réponse à la commande la plus ancienne en vol (le serveur répond toujours
dans l'ordre d'envoi) :
dead→sys.exit(0)immédiat (ai/src/network/command_queue.py:116) ;- sinon, on dépile la tête de
_inflight, on la mémorise comme_last_command, et on invoque son callback avec la ligne brute (ai/src/network/command_queue.py:136-141) ; - cas particulier : si
_inflightest vide mais_last_commandexiste, on ré-invoque le dernier callback — c'est ainsi qu'est capté leCurrent level: knon sollicité qui suit une incantation (ai/src/network/command_queue.py:128-134).
Le callback (celui que la FSM a fabriqué, voir Stratégie (FSM))
appelle parse_server_line (ai/src/protocol/parser.py:26) pour produire un
message typé, réinjecte la pos/direction capturée pour un LookResult, puis
le passe à state.on_response(...), qui met à jour DroneState et WorldMap
(voir État interne).
Phase de décision¶
Une fois l'état à jour, la boucle entre dans
while client.command_queue.can_send() (ai/src/main.py:135). Tant que moins de
MAX_PENDING_COMMANDS (= 10) commandes sont en vol, elle appelle
FSM.decide_next_command() :
- l'état courant peut demander une transition, puis produire une commande pointée
"cmd1.cmd2.cmd3"; - pour chaque sous-commande, la FSM applique ses mises à jour locales
optimistes (rotation, avance torique, dépôt de ressource) puis appelle
Client.send_command; send_command(ai/src/network/client.py:55) vérifiecan_send, poussecommande + "\n"dans l'OutputBuffer, enregistre la commande dans la file, et vide aussitôt le buffer sur la socket (flush_to, envoi synchrone).
Si decide_next_command ne renvoie rien à émettre, elle retourne False et la
boucle interne s'arrête (ai/src/main.py:136).
Condition d'arrêt¶
La boucle sort quand drone.is_dead() and config.INIT
(ai/src/main.py:142). Le drapeau config.INIT (ai/src/config.py:115) passe à
True au premier Inventory reçu (update_inventory) : il garantit qu'on ne
considère pas le drone « mort » tant que son inventaire initial (food = 0 par
défaut) n'a pas été renseigné par le serveur.
Diagramme de séquence (un tour complet)¶
sequenceDiagram
participant Boucle as main.run (boucle)
participant Client
participant CQ as CommandQueue
participant FSM
participant État as State courant
participant Srv as Serveur
Note over Boucle: --- Phase réception ---
Boucle->>Client: receive()
Client->>Srv: select(0.1) + recv(4096)
Srv-->>Client: lignes brutes
Client->>Client: InputBuffer.feed / pop_line
loop par ligne complète
Client->>CQ: on_response(line)
alt line == "dead"
CQ->>Boucle: sys.exit(0)
else réponse normale
CQ->>CQ: pop FIFO _inflight
CQ->>FSM: callback(line)
FSM->>FSM: parse_server_line + réinjecte pos/dir (Look)
FSM->>État: on_response(msg)
État->>État: MAJ DroneState / WorldMap
end
end
Note over Boucle: --- Phase décision ---
loop tant que CQ.can_send()
Boucle->>FSM: decide_next_command()
FSM->>État: should_transition() / next_command()
État-->>FSM: "cmd1.cmd2.cmd3" (ou None)
loop par sous-commande
FSM->>FSM: MAJ locale optimiste
FSM->>Client: send_command(sub, callback)
Client->>CQ: enqueue(sub, callback)
Client->>Srv: OutputBuffer.flush_to (sync)
end
end
Boucle->>Boucle: is_dead() and INIT ? -> sortie
Récapitulatif du flux¶
| Étape | Composant | Fichier |
|---|---|---|
| Lire la socket | Client.receive |
ai/src/network/client.py:84 |
| Découper en lignes | InputBuffer |
ai/src/network/buffers.py:38 |
| Apparier FIFO + callback | CommandQueue.on_response |
ai/src/network/command_queue.py:100 |
| Parser en message typé | parse_server_line |
ai/src/protocol/parser.py:26 |
| Mettre à jour l'état | State.on_response |
ai/src/strategy/states/ |
| Décider l'action | FSM.decide_next_command |
ai/src/strategy/fsm.py:101 |
| Émettre la commande | Client.send_command |
ai/src/network/client.py:55 |
Pour aller plus loin¶
- Réseau & file de commandes — handshake, buffers, plafond des 10 commandes en vol.
- Couche protocole — détail de
parse_server_lineet des messages typés. - Stratégie (FSM) — états, transitions et état local optimiste.
- État interne —
DroneState, vision et carte torique mises à jour.