Skip to content

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():

./zappy_ai -p <port> -n <team_name> -h <machine>
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
  1. Connection & handshakeClient.setup_connection opens the Connection, runs the handshake (WELCOME, sending the team name, reading CLIENT-NUM then X Y) and returns (nb_per_team, (width, height)).
  2. Initial stateDroneState(nb_per_team, world_size, client) places the drone at the centre of the map, faces it N, builds the WorldMap, and immediately sends Inventory and Look to seed the state (ai/src/state/drone.py:76-86).
  3. FSMFSM(drone, client) registers the states and starts in the survive state (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