Skip to content

Protocol layer

The protocol/ layer is the (de)serialization boundary between the network's raw strings and the client's typed state. It does no I/O: the network hands it lines, it returns objects, and vice versa.

Module File Role
parser ai/src/protocol/parser.py Raw server line → typed message
commands ai/src/protocol/commands.py Command builders (pure strings)
messages ai/src/protocol/messages.py Message dataclasses/enums
update ai/src/protocol/update.py State-update callbacks

parser — server lines to messages

parse_server_line(line, client) (ai/src/protocol/parser.py:26) routes each line through a match:

match line:
    case "ok":   return OkKo.OK
    case "ko":   return OkKo.KO
    case "dead": return DeadMessage()
    case _ if line.startswith("message "):           return parse_broadcast(line)
    case _ if line.startswith("eject: "):            return parse_eject(line)
    case _ if line.startswith("Elevation underway"): return ElevationResult(success=True, level=None)
    case _ if line.startswith("Current level: "):    ...  # -> ElevationResult(success=True, level=k)
    case _ if line.startswith("["):                  ...  # Look or Inventory
    case _:                                           raise ValueError(...)

Key points:

  • ok / ko become the OkKo enum; dead a DeadMessage.
  • Elevation underway and Current level: k both become an ElevationResult(success=True, ...), the latter carrying the new level.
  • [...] lines: ambiguous between Look and Inventory. Disambiguation relies on client.command_queue.peek_last_command() (ai/src/protocol/parser.py:54-60) — if the last command was Look it calls parse_look, if it was Inventory it calls parse_inventory.

parse_look & parse_inventory

parse_look (ai/src/protocol/parser.py:66) strips the brackets, splits on commas, then splits each tile on spaces → LookResult(tiles=...) (an empty tile yields an empty list).

parse_inventory (ai/src/protocol/parser.py:82) splits the same way but expects resource quantity pairs → InventoryResult(items={...}).

parse_eject (ai/src/protocol/parser.py:124) extracts the integer K after eject:EjectMessage(direction=K).

parse_broadcast is a stub

parse_broadcast (ai/src/protocol/parser.py:102) raises NotImplementedError: inbound broadcasts are not handled. Team coordination suffers as a result. See Known limitations.

commands — command builders

commands.py (ai/src/protocol/commands.py) provides pure functions that return the exact command string, without a trailing \n:

def forward() -> str:        return "Forward"
def look() -> str:           return "Look"
def inventory() -> str:      return "Inventory"
def broadcast(text) -> str:  return f"Broadcast {text}"
def take(obj) -> str:        return f"Take {obj}"
def set(obj) -> str:         return f"Set {obj}"
def incantation() -> str:    return "Incantation"
# … right, left, connect_nbr, fork, eject

The \n is added on send

These builders do not produce the trailing newline: it is Client.send_command that pushes command + "\n" into the OutputBuffer (see Network). Keeping this convention avoids double \n.

messages — typed messages

messages.py (ai/src/protocol/messages.py) defines the data structures — no logic, only containers (@dataclass(frozen=True) and one enum):

Type Contents
OkKo Enum OK / KO
LookResult tiles; optional pos and direction (re-injected by the FSM)
InventoryResult items: dict[str, int]
BroadcastMessage direction: int, text: str
EjectMessage direction: int
DeadMessage (empty)
ElevationResult success: bool, level: int \| None
ConnectNbrResult slots: int

Message is the union typing any parsed server line.

ConnectNbrResult is never produced

ConnectNbrResult is defined but no parser builds it, because reproduction (Fork / Connect_nbr) is not implemented. See Strategy (FSM) and Known limitations.

update — state-update callbacks

update.py (ai/src/protocol/update.py) provides the callbacks wired by DroneState at construction (see Internal state):

  • update_inventory(drone, client, line) (ai/src/protocol/update.py:15) parses the line, checks it really is an InventoryResult, then updates drone.inventory and drone.food. Crucially, it sets config.INIT = True — this flag is what later allows the main loop to exit on drone.is_dead() (the drone is not declared dead until the first inventory has arrived).
  • update_vision(drone, client, line) (ai/src/protocol/update.py:31) parses the line and stores the LookResult in drone.last_vision (otherwise None).

Going further