Skip to content

Decision loop

This page assembles all the previous layers into an end-to-end execution flow: from the raw incoming TCP packet to the action sent back to the server. It is the main loop in main.run() (ai/src/main.py:127) that orchestrates everything.

Overview

The loop is single-threaded and alternates two phases on each iteration:

  1. Receive — drain the socket, parse each line, update the state.
  2. Decide — while the command queue is not full, ask the FSM for the next action and emit it.
while 1:
    client.receive()                          # receive phase
    while client.command_queue.can_send():    # decide phase
        if not fsm.decide_next_command():
            break
    if drone.is_dead() and config.INIT:
        break

Select-with-timeout polling, not selectors

The docstrings in main.py and the README mention a selectors loop with no busy-wait. The real code uses a while 1 that polls the socket via select.select([sock], [], [], 0.1) (ai/src/network/client.py:89): a 100 ms poll, not the selectors module. See Known limitations.

The receive phase, step by step

Client.receive() (ai/src/network/client.py:84) reads the non-blocking socket as long as select reports it ready, in 4096-byte chunks. An empty recv means the server closed the connection (connection = None).

Each chunk goes through receive_data (ai/src/network/client.py:100):

  1. InputBuffer.feed(data) accumulates the raw bytes (ai/src/network/buffers.py:24);
  2. InputBuffer.pop_line() extracts the complete lines (separated by \n) one by one (ai/src/network/buffers.py:38);
  3. each line is passed as-is (unparsed) to CommandQueue.on_response(line) (ai/src/network/client.py:124).

Why a line buffer?

TCP is a byte stream with no message boundaries: one recv may deliver half a line, or three lines at once. The buffer restores the line splitting required by Zappy's text-oriented protocol.

FIFO matching and callback firing

CommandQueue.on_response (ai/src/network/command_queue.py:100) matches the response to the oldest in-flight command (the server always replies in send order):

  • dead → immediate sys.exit(0) (ai/src/network/command_queue.py:116);
  • otherwise, it pops the head of _inflight, stores it as _last_command, and invokes its callback with the raw line (ai/src/network/command_queue.py:136-141);
  • special case: if _inflight is empty but _last_command exists, the last callback is re-invoked — this is how the unsolicited Current level: k following an incantation is caught (ai/src/network/command_queue.py:128-134).

The callback (the one the FSM built, see Strategy (FSM)) calls parse_server_line (ai/src/protocol/parser.py:26) to produce a typed message, re-injects the captured pos/direction for a LookResult, then passes it to state.on_response(...), which updates DroneState and WorldMap (see Internal state).

The decide phase

Once the state is up to date, the loop enters while client.command_queue.can_send() (ai/src/main.py:135). As long as fewer than MAX_PENDING_COMMANDS (= 10) commands are in flight, it calls FSM.decide_next_command():

  • the current state may request a transition, then produce a dotted command "cmd1.cmd2.cmd3";
  • for each sub-command, the FSM applies its optimistic local updates (rotation, toric advance, resource drop) then calls Client.send_command;
  • send_command (ai/src/network/client.py:55) checks can_send, pushes command + "\n" into the OutputBuffer, registers the command in the queue, and immediately flushes the buffer to the socket (flush_to, synchronous send).

If decide_next_command has nothing to emit, it returns False and the inner loop stops (ai/src/main.py:136).

Stop condition

The loop exits when drone.is_dead() and config.INIT (ai/src/main.py:142). The config.INIT flag (ai/src/config.py:115) turns True on the first Inventory received (update_inventory): it guarantees the drone is not considered "dead" until its initial inventory (food = 0 by default) has been filled in by the server.

Sequence diagram (one full tick)

sequenceDiagram
    participant Loop as main.run (loop)
    participant Client
    participant CQ as CommandQueue
    participant FSM
    participant St as current State
    participant Srv as Server

    Note over Loop: --- Receive phase ---
    Loop->>Client: receive()
    Client->>Srv: select(0.1) + recv(4096)
    Srv-->>Client: raw lines
    Client->>Client: InputBuffer.feed / pop_line
    loop per complete line
        Client->>CQ: on_response(line)
        alt line == "dead"
            CQ->>Loop: sys.exit(0)
        else normal response
            CQ->>CQ: pop FIFO _inflight
            CQ->>FSM: callback(line)
            FSM->>FSM: parse_server_line + re-inject pos/dir (Look)
            FSM->>St: on_response(msg)
            St->>St: update DroneState / WorldMap
        end
    end

    Note over Loop: --- Decide phase ---
    loop while CQ.can_send()
        Loop->>FSM: decide_next_command()
        FSM->>St: should_transition() / next_command()
        St-->>FSM: "cmd1.cmd2.cmd3" (or None)
        loop per sub-command
            FSM->>FSM: optimistic local update
            FSM->>Client: send_command(sub, callback)
            Client->>CQ: enqueue(sub, callback)
            Client->>Srv: OutputBuffer.flush_to (sync)
        end
    end

    Loop->>Loop: is_dead() and INIT ? -> exit

Flow recap

Step Component File
Read the socket Client.receive ai/src/network/client.py:84
Split into lines InputBuffer ai/src/network/buffers.py:38
FIFO match + callback CommandQueue.on_response ai/src/network/command_queue.py:100
Parse into typed message parse_server_line ai/src/protocol/parser.py:26
Update the state State.on_response ai/src/strategy/states/
Decide the action FSM.decide_next_command ai/src/strategy/fsm.py:101
Emit the command Client.send_command ai/src/network/client.py:55

Going further