BotStrikes

demo · 64 Hz · tick 0004096

Documentation for AI developers

Build an AI for BotStrikes

In BotStrikes an AI is not an in-game bot: it is a player. It connects to the same server as people, sends the same command as a keyboard and mouse, and receives only what the server authorizes it to see, under the same rules as a person. This guide explains the rules, the protocol and what the SDK will bring.

Status · Sep 12, 2026

Protocol v2 works on the development server: networked movement and shooting, with encrypted transport, and the server already applies perception and the provisional hand limits to AIs (F4). The C library, the Python SDK and the gym already work in the development repository, which is still private. There are no public servers or SDK download yet. What is marked Implemented is tested on that server; what is marked Design may change before it is published.

How an AI competes

There are no shortcuts: the server has no special hooks for AIs. Every restriction, from what it perceives to how fast it turns or how long it takes to react, is enforced by the server, so that not even malicious code can see or do more than its league allows. Today it already enforces line of sight, one command per tick, all the shooting rules and, for AIs, the field of view and the provisional hand limits.

  1. Your AI receives a signed connection token for a specific match. The token carries its role (AI): with it, the server applies its perception and its limits. In tournaments, the platform will issue it.
  2. It connects over UDP with the encrypted and authenticated transport of netcode 1.02.
  3. On every tick (64 per second, 15.625 ms) it sends a UserCmd: held keys, view angles, weapon and the tick at which it was seeing the others.
  4. It receives snapshots with its own exact state, only the players it can see and the results the server decided for it.

What your AI sees

Today Implemented

The snapshot carries your exact state: position, velocity, health, armor, ammo and the state of your weapon. An AI receives other players only if a person would see them on their screen: within the field of view of the reference client (75° vertical at 16:9, about 107.5° horizontal, oriented according to its last command), with line of sight to one of their hitboxes and if the visible part covers at least 2 × 2 pixels at 1080p (a range of about 280 m). Every material blocks sight and there is no margin: what it does not perceive does not travel over the network. Dead players are not sent, and while you are dead you receive no one. For people, line of sight is applied, without a field of view. An independent oracle, sharing no code with the server, reviewed 1,000,000 random scenes and found no leaks.

Shot results arrive as events decided by the server: your hit, the damage you take (without attacker or direction) and your death. It helps to know what they reveal: a hit on someone you don’t see, through cover, only gives a marker where the bullet entered the cover, without saying whom or how much damage; and your death tells you who caused it. If your AI plays under the motor model, the results of its shots (whom it hit and where) reach it with the same delay as its perception, and a hit on someone it does not perceive is only a marker.

What the AI observation will add Design

The full observation will follow the same information contract that limits a person:

Later on, a “Pixels” league will be evaluated, where the AI receives only the rendered image and the audio.

What it can do

The same as a person with a keyboard and mouse: one UserCmd per tick with held keys (forward, back, left, right, jump, crouch, walk, fire, reload, use), view angles and requested weapon. There is no field for positions, velocities, hits or damage: the server computes all of that.

Hand limits Implemented

From the moment a person joins the match until it ends, the server clamps the AIs’ commands with a motor model, without rejecting them, and counts each clamp. In the Free AI league, among AIs only, there are no hand limits; the information is always that of a person.

LimitInitial valueHow it is calibrated
Perception delay150 msReaction times of real players
Maximum angular velocity1,800 °/s99.9th percentile of recorded human turns
Maximum angular acceleration30,000 °/s²Same
Aiming errorProportional to turn speed when firing (Fitts’s law). Pending calibration (H5)Accuracy of human flicks
State changes per key≤ 15 per secondHuman key-press speed

These are provisional values: the final ones will come from player telemetry, will be validated by the founder (H5) and will be published each season. Because the world reaches it 10 ticks late (about 156 ms), the rewind for an AI under the motor model is reduced to 2 ticks: your AI must anticipate moving targets. Today the server does not send you back the clamped view: if your AI turns faster than the limit, its prediction drifts, so it is best to turn within the published limits. The same model works in reverse: it detects people with superhuman inputs.

Leagues

LeagueWho playsLimits
Free AIAIs onlySame information and same controls; no motor model
Human-equivalentAIsInformation + motor model. Required to play with or against people
HumansPeople
MixedPeople, AIs or combined teamsAIs with motor model

The ranking will be Glicko-2, separate for each league.

Protocol v2 Implemented

Development version

v2 is the development server’s protocol and replaced v1, which never left the development machine. It may change before public servers open; the SDKs will follow the current version. Pending: delta compression, reconnection and a public commitment of the secret for verifiable replays.

Conventions: little-endian; f32 is IEEE 754 binary32 transmitted bit for bit; no message allows trailing bytes or non-zero reserved fields, and an invalid message is discarded in full. Units: meters, seconds and degrees.

Transport. Messages travel as the payload of netcode 1.02, an open connection standard over UDP: signed connection token, challenge against spoofed addresses, encrypted and authenticated packets, replay protection and a payload of up to 1,200 bytes. One message per packet. BotStrikes does not use its own cryptography.

Content identity Pending A 64-bit FNV-1a hash over map, rules and weapons will identify the build, and a mismatch will reject the connection. Its exchange during connection is not implemented yet. It only detects different builds: it is not a security mechanism.

UserCmd, 14 bytes

The only thing a player can send, person or AI.

OffsetTypeFieldRule
0u32sequenceGoes up by one each tick; the first one received sets the start (≤ 2³¹−1)
4u16buttonsBits 0–9: forward, back, left, right, jump, crouch, walk, fire, reload, use. Bits 10–15 set to zero
6u16yaw65,536 units per turn
8i16pitch1/256 of a degree, positive looks down; maximum ±22,784 (±89°)
10u8weapon0 keeps the weapon; 1 rifle; 2 pistol
11u8reserved0
12u16view_tickLow 16 bits of the server tick at which you were drawing the others

These are held keys: the server deduces presses by comparing consecutive commands, and a command repeated because of loss never creates a press or a weapon change. Your client must predict its own movement with the command as it is encoded, not with its floating-point view.

view_tick is the tick of the newest snapshot minus 2 (the client’s interpolation). The server expands it to the nearest full tick, caps it to the last 12 ticks (187.5 ms) and does not let it go back relative to the previous command. In addition, the declared delay (current tick − view_tick) cannot exceed its minimum over the last second by more than 4 ticks: nobody can jump back in time right as they fire. A sustained delay is handled as a genuinely slow connection: it is an accepted residual risk, because no server can prove that a declared latency is false, and the 187.5 ms window bounds it, within the 200 ms cap. For an AI under the motor model the window is 2 ticks.

CommandPacket, 6 + 14·n bytes (client → server)

OffsetTypeFieldRule
0u8type1
1u8n1–4; clients send their 3 most recent commands, as redundancy against loss
2u32snapshot_ackLast server tick received
6UserCmd × ncommandsConsecutive ascending sequences

How the server applies your commands

Snapshot, 75 + 18·m bytes + events (server → client)

OffsetTypeFieldRule
0u8type2
1u8self_id0–15
2PlayerStateown state71 bytes
73u8m0–15 visible players
74RemotePlayer × mvisibleIds 0–15, no duplicates and not your own
u8e0–8 events
Event × eeventsConsecutive ids (modulo 65,536)

PlayerState, 71 bytes. The state of the selected weapon is sent in full so that your client reconciles fire rate, ammo and recoil just like movement. For the stowed weapon, its ammo and its wait are sent: switching weapons never shortens a wait.

OffsetTypeFieldRule
0u32tickServer tick
4u32last_commandLast sequence applied
8f32 × 3positionFinite, |x| ≤ 16,384
20f32 × 3velocityFinite, |v| ≤ 1,000
32u8flagsBit 0 on ground, 1 crouched, 2 jump held, 3 alive, 4 trigger held; rest 0
33u8selected weapon1–2
34f32healthFinite, 0–1,000; alive if and only if health > 0
38f32armorFinite, 0–1,000
42u16, u16, u8weapon 1Ammo, reserve and ticks until it can fire
47u16, u16, u8weapon 2Same; a stowed weapon keeps its wait
52u8ticks without firingSelected weapon; saturates at 255
53u8position in the patternSaturates at 255
54u16reloadTicks remaining
56f32 × 2recoilVertical and horizontal in degrees, |x| ≤ 90
64u16respawnTicks until respawn; 0 if alive
66u16 × 2scoreKills and deaths
70u8leadYour commands that were waiting on the server ahead of their tick; saturates at 255

RemotePlayer, 18 bytes: u8 id, f32 × 3 position, u16 yaw, i16 pitch, u8 flags (bit 0 crouched). A player who is not authorized to be seen does not appear: the format has no way to carry someone hidden. A player is visible if some part of their box, or of the arms that stick out of it, is in line of sight: everything that can be hit can be seen.

Events: what the server decided for you

Common header: u16 id, u8 tipo. Pending events travel in every snapshot, oldest first and up to 8, until your client acknowledges with snapshot_ack a snapshot that carried them. Your client processes each id only once.

TypeBytesContent
1 · own shot19u8 flags (bit 0 went through cover, bit 1 killed, bits 2–3 zone, bit 4 hit someone not visible), u8 player hit (255 = none), u16 damage in hundredths after armor, f32 × 3 point where the bullet stopped
2 · damage taken6u8 zone, u16 damage in hundredths. Without attacker or direction
3 · own death4u8 who caused it (255 = the world)

A hit through cover on someone you don’t see (bit 4) is only a marker: no player, zone or damage, and the position is where the bullet entered the cover.

Example: encoding commands in Python

Without an SDK, this is all you need to build the bytes a player sends. To connect you also need a netcode 1.02 client and a token; that is what the SDK will solve.

Python 3 · standard library only · protocol v2
import math
import struct

# UserCmd.buttons: keys held during this tick
FORWARD, BACK, LEFT, RIGHT = 1 << 0, 1 << 1, 1 << 2, 1 << 3
JUMP, CROUCH, WALK, FIRE = 1 << 4, 1 << 5, 1 << 6, 1 << 7
RELOAD, USE = 1 << 8, 1 << 9
KEEP, RIFLE, PISTOL = 0, 1, 2

# Quantization identical to the C++ client's: if your view differs by one
# bit, your prediction and the server simulate different commands.
def f32(x):
    """The client stores the view as a float: convert it to 32 bits first."""
    return struct.unpack("<f", struct.pack("<f", x))[0]

def lround(x):
    """Like std::lround: ties round away from zero (round() does not)."""
    a = abs(x)
    n = math.floor(a)
    if a - n >= 0.5:
        n += 1
    return int(math.copysign(n, x))

def yaw_units(degrees):
    """65,536 units per turn."""
    if not math.isfinite(degrees):
        return 0
    turns = math.remainder(f32(degrees), 360.0) / 360.0
    if turns < 0:
        turns += 1
    return lround(turns * 65536.0) & 0xFFFF

def pitch_units(degrees):
    """1/256 of a degree, positive looks down, limit ±89°."""
    if not math.isfinite(degrees):
        return 0
    return lround(max(-89.0, min(89.0, f32(degrees))) * 256.0)

def user_cmd(sequence, buttons, yaw_deg, pitch_deg, view_tick, weapon=KEEP):
    """UserCmd v2: 14 bytes little-endian, identical for people and AIs."""
    return struct.pack("<IHHhBBH", sequence, buttons, yaw_units(yaw_deg),
                       pitch_units(pitch_deg), weapon, 0, view_tick & 0xFFFF)

def command_packet(snapshot_ack, cmds):
    """CommandPacket (type 1): 1 to 4 commands with consecutive sequences."""
    assert 1 <= len(cmds) <= 4
    return struct.pack("<BBI", 1, len(cmds), snapshot_ack) + b"".join(cmds)

# view_tick: tick of the newest snapshot minus 2 (the client's interpolation)
cmd = user_cmd(1, FORWARD | FIRE, 90.0, 0.0, view_tick=4091, weapon=RIFLE)
assert cmd.hex(" ") == "01 00 00 00 81 00 00 40 00 00 01 00 fb 0f"

SDK, gym and reference bots In progress

LLMs are too slow to control tick by tick, but they can be the strategist inside your AI’s process.

Tournaments

Want to put up a prize, or have evidence that challenges these rules? Write to contacto@botstrikes.com.

Changes