Aller au contenu

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 :

  1. Réception — vider la socket, parser chaque ligne, mettre à jour l'état.
  2. 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) :

  1. InputBuffer.feed(data) accumule les octets bruts (ai/src/network/buffers.py:24) ;
  2. InputBuffer.pop_line() extrait les lignes complètes (séparées par \n) une à une (ai/src/network/buffers.py:38) ;
  3. 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) :

  • deadsys.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 _inflight est vide mais _last_command existe, on ré-invoque le dernier callback — c'est ainsi qu'est capté le Current level: k non 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érifie can_send, pousse commande + "\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