AI (Python) — Overview¶
The Zappy AI client is an autonomous drone written in Python (3.11+). It connects to the game server over a TCP socket, observes its surroundings, makes its own decisions and tries to level up its team through incantations.
No external dependencies
The project uses only the standard library: pyproject.toml declares
dependencies = [] and requires-python >= 3.11. No networking framework,
no selectors, no asyncio — just socket, select, argparse,
dataclasses, etc. (ai/pyproject.toml).
Launching¶
The client is invoked through the zappy_ai launcher, which inserts src/
into sys.path and then calls main():
| Option | Purpose | Default |
|---|---|---|
-p/--port |
Server TCP port | none |
-n/--name |
Team name (handshake) | none |
-h/--host |
Server machine address | 127.0.0.1 |
--debug |
Enable debug logging | disabled |
“Required” arguments are not enforced
The subject describes -p and -n as mandatory, but parse_args
(ai/src/main.py:45-57) does not set required=True: their default value
is None. Likewise, config.DEFAULT_HOST = "localhost"
(ai/src/config.py:106) is unused — the effective default is 127.0.0.1.
See Known limitations.
Layered architecture¶
The code is organised into clean layers, from the lowest level (bytes on the wire) to the highest level (strategic decisions).
flowchart TD
main["main.py<br/>(entry point, loop)"]
subgraph strategy["strategy/ — decision"]
fsm["FSM"]
states["states/<br/>survive, rally, collect,<br/>explore, incant_prep, incant…"]
path["pathfinding<br/>incantation"]
end
subgraph state["state/ — internal state"]
drone["DroneState"]
wmap["WorldMap"]
inv["inventory / vision"]
end
subgraph protocol["protocol/ — serialization"]
parser["parser"]
commands["commands"]
messages["messages"]
update["update"]
end
subgraph network["network/ — transport"]
client["Client"]
conn["Connection"]
cq["CommandQueue"]
buf["InputBuffer / OutputBuffer"]
end
team["team/ — coordination<br/>(crypto, protocol, decoy)"]
utils["utils/ — logger"]
main --> strategy
main --> network
strategy --> state
strategy --> protocol
state --> protocol
protocol --> messages
network --> protocol
network --> conn
network --> cq
network --> buf
strategy -.->|planned, not wired| team
main --> utils
The team/ layer is not wired in
The team/ package (encryption, inter-drone protocol, decoys) is fully
implemented but never called by the network or strategy layers. See
Team coordination and Known limitations.
Source tree¶
ai/
├── zappy_ai # executable launcher (inserts src/ into sys.path)
├── pyproject.toml # zappy_ai, requires-python >=3.11, deps=[]
└── src/
├── main.py # parse_args, run(): main loop
├── config.py # subject constants (costs, requirements, thresholds)
├── network/
│ ├── connection.py # TCP socket + handshake
│ ├── client.py # send/receive orchestration
│ ├── command_queue.py # in-flight 10-command queue (FIFO)
│ └── buffers.py # splits the byte stream into lines
├── protocol/
│ ├── parser.py # server lines -> typed messages
│ ├── commands.py # command builders (pure strings)
│ ├── messages.py # message dataclasses
│ └── update.py # state-update callbacks
├── state/
│ ├── drone.py # DroneState (level, food, position…)
│ ├── inventory.py # inventory helpers
│ ├── vision.py # vision-cone geometry
│ └── world_map.py # toroidal mental map
├── strategy/
│ ├── fsm.py # state machine + command driving
│ ├── incantation.py # elevation requirements
│ ├── pathfinding.py # toroidal distances/moves
│ └── states/ # 8 concrete FSM states
├── team/ # crypto.py, protocol.py, decoy.py (not wired)
└── utils/
└── logger.py
Startup flow¶
The run(args) function (ai/src/main.py:83) chains:
sequenceDiagram
participant M as main.run
participant C as Client
participant Cx as Connection
participant D as DroneState
participant F as FSM
M->>C: setup_connection(host, port, name)
C->>Cx: connect() then do_handshake()
Cx-->>C: (nb_per_team, (W, H))
C-->>M: (nb_per_team, (W, H))
M->>D: DroneState(nb_per_team, (W,H), client)
Note over D: immediately sends<br/>Inventory + Look
M->>F: FSM(drone, client)
M->>F: transition_to("survive")
loop while alive
M->>C: receive() (reads, routes to callbacks)
loop while command_queue.can_send()
M->>F: decide_next_command()
end
end
- Connection & handshake —
Client.setup_connectionopens theConnection, runs the handshake (WELCOME, sending the team name, readingCLIENT-NUMthenX Y) and returns(nb_per_team, (width, height)). - Initial state —
DroneState(nb_per_team, world_size, client)places the drone at the centre of the map, faces itN, builds theWorldMap, and immediately sendsInventoryandLookto seed the state (ai/src/state/drone.py:76-86). - FSM —
FSM(drone, client)registers the states and starts in thesurvivestate (ai/src/main.py:124-125).
Initial state is survive, not explore
The README and some diagrams describe a start in explore. The code
actually starts in survive. See Known limitations.
Main loop¶
while 1:
client.receive() # read the network, route responses
while client.command_queue.can_send(): # while slots remain
if not fsm.decide_next_command(): # ask the FSM for an action
break
if drone.is_dead() and config.INIT: # confirmed starvation
break
The loop (ai/src/main.py:127-144) receives first (server responses update
the state through callbacks), then asks the FSM for commands as long as the
in-flight command queue is not full (cap of 10).
Busy loop, no selectors multiplexing
Despite the docstrings, there is no busy-wait-free selectors loop: it is a
while 1 calling select.select(..., 0.1) (a 100 ms poll). See
Known limitations.
Going further¶
- Network & command queue — TCP transport, handshake, in-flight command FIFO.
- Protocol layer — parsing server lines, command builders, typed messages.
- Internal state —
DroneState, inventory, vision, toroidal map. - Strategy (FSM) — state machine and optimistic local state.
- Team coordination — encryption and inter-drone protocol.
- Decision loop — from the network packet to the action.