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:
- Receive — drain the socket, parse each line, update the state.
- 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):
InputBuffer.feed(data)accumulates the raw bytes (ai/src/network/buffers.py:24);InputBuffer.pop_line()extracts the complete lines (separated by\n) one by one (ai/src/network/buffers.py:38);- 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→ immediatesys.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
_inflightis empty but_last_commandexists, the last callback is re-invoked — this is how the unsolicitedCurrent level: kfollowing 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) checkscan_send, pushescommand + "\n"into theOutputBuffer, 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¶
- Network & command queue — handshake, buffers, the 10 in-flight command cap.
- Protocol layer — details of
parse_server_lineand typed messages. - Strategy (FSM) — states, transitions and optimistic local state.
- Internal state —
DroneState, vision and toric map updates.